From 98ecb2c907105ad70532ea38a63513d8e67dfd6c Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 15:11:30 +0200 Subject: [PATCH 1/7] release: prepare v0.21.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bump workspace version 0.21.1 → 0.21.2 - Consolidate duplicate CHANGELOG.md section headers in [0.21.2] - Update splash snapshot for new version string - Update test badge count (9824) - Update specs to reflect changes since v0.21.1 --- CHANGELOG.md | 581 ++++++++---------- Cargo.lock | 60 +- Cargo.toml | 2 +- README.md | 2 +- ...idgets__splash__tests__splash_default.snap | 3 +- specs/002-agent-loop/spec.md | 64 ++ specs/004-memory/spec.md | 58 ++ specs/005-skills/spec.md | 45 ++ .../010-7-shadow-memory-guardrail.md | 18 +- specs/010-security/spec.md | 71 +++ specs/011-tui/spec.md | 53 ++ specs/012-graph-memory/spec.md | 36 ++ specs/013-acp/spec.md | 36 ++ specs/021-zeph-context/spec.md | 69 ++- specs/028-hooks/spec.md | 49 ++ specs/037-config-schema/spec.md | 33 + specs/044-subagent-lifecycle/spec.md | 57 +- specs/058-plugins/spec.md | 69 ++- specs/README.md | 6 +- 19 files changed, 955 insertions(+), 357 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0da0f2891..1f9825c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [0.21.2] - 2026-05-18 + ### Added - `zeph-config`: Added `telemetry.trace_metadata` config field (`HashMap`) @@ -33,19 +35,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - ACC (#4081): `MemCotConfig::max_state_chars` enforcement was already present in `zeph-core/src/agent/memcot/accumulator.rs`. No code changes required; closes #4081. -### Security - -- `zeph-core`, `zeph-config`: Parent messages passed to spawned sub-agents are now sanitized - through the IPI pipeline by default (`parent_context_policy = "inherit_sanitized"`), stripping - prompt-injection payloads that may have entered the parent history via tool results, web scrapes, - or A2A messages (closes #3942, #3936). -- `zeph-config`: Added `max_parent_messages` config cap (default 20) for sub-agent spawn context - to limit the blast radius of poisoned histories (closes #3936). -- `zeph-config`: Added `ParentContextPolicy` enum (`inherit` / `inherit_sanitized` / `none`) - giving operators explicit control over cross-agent context propagation trust (closes #3936). - -### Added - - `zeph-memory`: New `agent_sessions` table (SQLite migration 089 / PostgreSQL 090) with `upsert_agent_session`, `update_agent_session_status`, `reconcile_stale_sessions`, and `list_agent_sessions` methods on `SqliteStore`. Session kind/status/channel are typed enums @@ -68,59 +57,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). title updates are emitted; `TuiConfig.fleet: FleetConfig` exposes `refresh_interval_secs` (default 5) and `max_sessions` (default 50) for the fleet panel. -### Fixed - -- `zeph-core`: `AutonomousRegistry::upsert`, `remove`, and `list` now log a `tracing::error!` - and recover the guard via `into_inner()` instead of silently discarding poisoned-mutex errors - (closes #4323). -- `zeph-core`: Extracted `apply_supervisor_backoff` as a pure free function operating on - `&mut AutonomousSession`; `increment_supervisor_fail_count` is now testable without a full - agent mock, and three unit tests cover the backoff boundary conditions (closes #4321). - `build_conversation_summary` was already a private helper method; no source duplication - remained (closes #4322). -- `zeph-orchestration`: `OrchestrationConfig.default_failure_strategy` was parsed from TOML but - never applied to the produced `TaskGraph`. `LlmPlanner::new` now parses the config string into - `FailureStrategy` and sets `graph.default_failure_strategy` on every graph it constructs; - unrecognised values fall back to `Abort` with a warning log (closes #4324). -- `zeph-llm`: `BanditState::load`, `ReputationTracker::load`, and `ThompsonState::load` in - `RouterProvider` builder methods were calling `std::fs::read` directly on the Tokio executor - thread; wrapped in a `blocking_load()` helper using `tokio::task::block_in_place` to avoid - stalling the thread pool during startup (closes #4296). -- `zeph-memory`: Added `tokio::time::timeout` guards (5 s, fail-open) to all remaining - `embed()` call sites in `semantic/recall.rs`, `semantic/mod.rs`, `semantic/summarization.rs`, - `semantic/cross_session.rs`, and `reasoning.rs`; the batch `join_all` in `semantic/graph.rs` - received a 30 s global timeout — prevents a stalled embedding provider from blocking agent - turns indefinitely (closes #4297). -- `zeph-tools`: `RiskChainAccumulator` is now instantiated at agent startup and wired to - `ShellExecutor` via `with_risk_chain` and to the agent builder via - `with_risk_chain_accumulator`. Multi-step shell attack chain detection is active at runtime - for the first time (closes #4273). -- `zeph-memory`: Removed the unchecked `delete_acp_session` from `AcpSessionStore`; all - callers migrated to `delete_acp_session_checked` which returns whether a row existed and - eliminates the TOCTOU race (closes #4279). - -### Changed - -- `zeph-config`: `AuditConfig.destination` changed from `String` to the typed `AuditDestination` - enum (`Stdout`, `Stderr`, `File(PathBuf)`); invalid destination values are now rejected at - deserialization time instead of silently opening a file at the mistyped path (closes #4302). -- `zeph-config`: `StoreRoutingConfig.fallback_route` changed from `String` to `MemoryRoute` - enum; invalid route values are now rejected at deserialization time instead of silently - falling back to `Hybrid` at runtime (closes #4301). -- `zeph-acp`: `SessionStatus` enum marked `#[non_exhaustive]` — downstream match arms must - include a `_ =>` wildcard to remain compatible with future variants (closes #4264). -- `zeph-sanitizer`: `NliSanitizer::circuit_is_open` renamed to - `check_and_maybe_reset_circuit` to make the cooldown-reset side effect explicit (closes #4251). - -### Performance - -- `zeph-sanitizer`: `SecretMaskRegistry` counter replaced with `AtomicUsize` (lock-free - increment); `mask()` now uses a pre-sorted `Vec` cache updated on `register()`, reducing - per-call complexity from O(n log n) to O(n). Concurrent `register()` correctness preserved - by holding `forward.write()` across the full check-and-insert path (closes #4248). - -### Added - - `/goal create ... --auto [--turns N]` — autonomous multi-turn goal execution: agent runs without user input until goal condition is met or turn limit reached (`AutonomousDriver`, cooperative scheduling in agent select loop). @@ -171,39 +107,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `zeph-config`: `ScrapeConfig.ipi_filter_threshold` field (default 0.6) allows per-deployment tuning of the IPI detection threshold. -### Fixed - -- `zeph-core`: `update_provider_instructions` now wraps `load_instructions_async` in a 5 s - timeout; on expiry logs a warning and keeps the previous instruction blocks, preventing an - indefinite hang on the provider-switch hot path (closes #4257). -- `zeph-config`: `SchedulerConfig::default().enabled` changed from `true` to `false`, matching - all other subsystem config defaults so the scheduler no longer activates on a freshly created - config with no `[scheduler]` section (closes #4255). -- `zeph-skills`: `SkillMiner::embed_existing` now wraps each `embed_provider.embed()` call in - `tokio::time::timeout(generation_timeout_ms)`; on timeout logs a warning and skips the skill - instead of blocking the entire mining run (closes #4254). - -- `zeph-agent-feedback`: CJK rejection pattern `^違う` now requires a terminal punctuation - character, whitespace, or end-of-message so that neutral phrases like "違う質問があります" - ("I have a different question") are no longer classified as ExplicitRejection (closes #3826). -- `zeph-channels`: `DiscordChannel` and `SlackChannel` now implement `Channel::send_status`, - forwarding status text as a plain message to the configured channel; previously the default - no-op was used, silently dropping all status updates (closes #3825). -- `zeph-memory`: consolidation LLM timeout is now configurable via `ConsolidationConfig.llm_timeout_secs` - (default 30s); previously hardcoded to 30s (closes #4168). -- `zeph-memory`: graph extractor LLM timeout is now configurable via `GraphExtractionConfig.llm_timeout_secs` - and `GraphConfig.llm_timeout_secs` (default 30s); previously hardcoded to 30s (closes #4169). -- `zeph-memory`: summarization LLM timeout is now configurable via `MemoryConfig.summarization_llm_timeout_secs` - (default 60s); previously hardcoded to 60s in both the structured and plain-text fallback paths (closes #4170). -- `zeph-memory`: added `#[tracing::instrument]` span to `compute_semantic_novelty` in - `admission.rs` so embedding latency during admission is visible in local Chrome JSON traces - (closes #4145). -- `zeph-agent-feedback`: wrap `JudgeDetector::evaluate` LLM call in 30 s `tokio::time::timeout`; add `JudgeError::Timeout` variant (closes #4179). -- `zeph-channels`: wrap Discord gateway `connect_async` (10 s), Hello receive (30 s), and interaction ACK (3 s) in `tokio::time::timeout` guards (closes #4180). -- `zeph-channels`: wrap all Slack Web API HTTP calls (`auth_test`, `post_message`, `update_message`, `download_file`) in 15 s `tokio::time::timeout` guards (closes #4181). - -### Added - - `specs/057-agent-persistence/spec.md` — comprehensive specification for `zeph-agent-persistence` crate: stateless `PersistenceService`, borrow-lens views, history load with tool-pair sanitization, message persistence, embedding decisions, exfiltration guard integration, and key durability invariants @@ -250,69 +153,87 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `skills.semantic_scan_provider` (provider name, default empty) config fields for opt-in LLM-backed SKILL.md compliance scan (closes #3959). -### Changed (behavior fix) - -- `zeph-core`: `PreToolUse` hooks with `fail_closed = true` now correctly block tool execution when - the hook returns an error; previously hook errors were logged but the tool proceeded (fail-open - behavior despite `fail_closed` setting). This is a semantic behavior change — tools guarded by - `fail_closed` hooks that fail intermittently will no longer execute silently (closes #3995). - -### Changed - -- `zeph-skills`: `SkillEmbedding::from_raw` visibility narrowed to `pub(crate)` — it is an - internal embedding-provider boundary helper and should not be part of the public API - (closes #3958). +- `zeph-scheduler`: `Scheduler::with_handler_timeout(Duration)` builder method sets a + maximum execution time for task handlers. Defaults to 300 seconds; pass `Duration::ZERO` + to disable. Hung handlers are now cancelled with a `SchedulerError::TaskFailed` instead + of blocking the tick loop indefinitely (closes #3944). +- `zeph-scheduler`: `SchedulerDaemonConfig.handler_timeout_secs` config field (default 300, + set to 0 to disable). Applied automatically by `run_foreground` and the agent bootstrap. +- `zeph-scheduler`: `scheduler.daemon.tick` tracing spans on every tick in `run()`, + `run_with_interval()`, and `run_with_interval_and_grace()`, and `scheduler.task.execute` + spans around each handler invocation in `tick()` and `catch_up_missed()` (closes #3945). -### Performance +- feat(memory,agent): wire `ScrapMem` optical forgetting loop into agent startup (issue #3910). + `start_optical_forgetting_loop` is now spawned via `TaskSupervisor` when + `memory.optical_forgetting.enabled = true`. Provider is resolved from + `optical_forgetting.compress_provider` (falls back to primary). Added + `build_optical_forgetting_provider` to `bootstrap`. Fixed `compress_content` / + `summarize_content` instrumentation to use `#[tracing::instrument]` (was `Entered` guard + through await, making the future `!Send`). -- `zeph-gateway`: added `tracing::instrument` spans (`gateway.webhook`, `gateway.health`) to HTTP - handler functions for latency visibility in traces (closes #3906). -- `zeph-commands`: added `tracing::info_span!` instrumentation to all slash command `handle()` - implementations; span name format `commands..handle` (closes #3927). -- `zeph-context`: added `tracing::instrument` spans (`context.persona_facts`, - `context.trajectory_hints`, `context.tree_memory`) to the three async context fetchers in the - assembler (closes #3984). -- `zeph-context`: `run_chunk_summaries` now converts the `guidelines` string to `Arc` once - before the chunk iterator, replacing a `String` clone per chunk with a cheap `Arc` clone (closes #3991). -- `zeph-context`: added `tracing::instrument` spans to the remaining 7 uninstrumented `fetch_*` - functions in the assembler (`context.graph_facts`, `context.reasoning_strategies`, - `context.corrections`, `context.semantic_recall`, `context.document_rag`, `context.summaries`, - `context.cross_session`), completing full hot-path trace coverage (closes #4092). -- `zeph-memory`: added `#[tracing::instrument(skip_all)]` to 6 hot-path async functions in - `skills.rs` (`record_skill_usage`, `record_skill_outcomes_batch`, `active_skill_version`, - `activate_skill_version`, `increment_heuristic_use_count`, `save_routing_head_weights`) and - 2 functions in `graph_store.rs` (`save_graph`, `load_graph`) for trace visibility (closes #4129). +- feat(memory,agent): wire `MemFlow` tiered retrieval into agent semantic recall path (issue + #3911). `inject_semantic_recall` in `ContextService` now dispatches to `recall_tiered` when + `memory.tiered_retrieval.enabled = true`, falling back to `fetch_semantic_recall_raw` when + disabled. Providers (`classifier_provider`, `validator_provider`) are resolved at agent + construction via `build_tiered_retrieval_classifier_provider` / + `build_tiered_retrieval_validator_provider` in `bootstrap` and stored in + `MemoryPersistenceState`. Added `with_tiered_retrieval_providers` builder method to + `Agent`. -### Fixed +- feat(memory): implement MemFlow tiered intent-driven retrieval (issue #3712). New + `tiered_retrieval` module in `zeph-memory` classifies incoming queries into three tiers — + `ProfileLookup`, `TargetedRetrieval`, and `DeepReasoning` — via the existing `MemoryRouter` + trait and dispatches to `SemanticMemory` with per-tier token budgets. Optional LLM validation + step (configurable via `[memory.tiered_retrieval]`) re-scores retrieved messages and can + escalate to the next tier when evidence is insufficient. Configuration fields: + `enabled`, `classifier_provider`, `validator_provider`, `token_budget`, `validation_enabled`, + `validation_threshold`, `max_escalations`. -- `zeph-memory`: `increment_heuristic_use_count` and `save_routing_head_weights` in - `skills.rs` now have `#[cfg(not(feature = "postgres"))]` / `#[cfg(feature = "postgres")]` - variants; the postgres variant uses `CURRENT_TIMESTAMP` instead of the SQLite-only - `datetime('now')` (closes #4126). +- feat(memory): introduce `EntityId(i64)` and `ExperienceId(i64)` newtype wrappers (issue #3795). + `GraphStore::upsert_entity` now returns `(i64, EntityId)` — the raw `i64` is used at all call + sites requiring plain DB IDs; the `EntityId` newtype is stored on `Entity.id` to prevent silent + swaps between entity IDs and other integer types across the graph subsystem. -- `zeph-core`: `/graph` command handlers now distinguish "graph enabled but vector store unavailable - (Qdrant unreachable)" from "Graph memory is not enabled." when `memory.graph.enabled = true` but - Qdrant is down, resolving the inconsistency with `/status` output (closes #4111). -- `zeph-commands`: `ClearQueueCommand` now logs a `tracing::debug!` message when - `send_queue_count` fails instead of silently discarding the error (closes #4115). +- feat(memory): implement episodic-to-semantic consolidation daemon (issue #3799). New + `EpisodicConsolidationDaemon` background loop periodically sweeps unconsolidated + `episodic_events` rows, extracts durable facts via LLM, deduplicates with Jaccard token + overlap, persists to `consolidated_facts` / `consolidated_fact_sources` tables (migration 087), + optionally promotes high-confidence facts to the Qdrant `zeph_key_facts` semantic tier, and + marks processed events with `consolidated_at`. Controlled by `[memory.episodic_consolidation]` + config section: `enabled`, `consolidation_provider`, `interval_secs`, `batch_size`, + `min_age_secs`, `dedup_jaccard_threshold`. -### Documentation +- feat(memory): implement ScrapMem optical forgetting and EM-Graph (issue #3713). New + `optical_forgetting` module progressively compresses old messages through three fidelity + levels — `Full` → `Compressed` → `SummaryOnly` — by scheduling background LLM sweeps. + The sweep skips messages below the `SleepGate` forgetting floor. New `episodic_graph` + module extracts episodic events from conversation turns and builds a causal graph + (`episodic_events` + `causal_links` tables added via migration 086, including + `UNIQUE(cause_event_id, effect_event_id)` for idempotent insertion). Both features are + off by default; enabled via `[memory.optical_forgetting]` and `[memory.em_graph]` + config sections. -- `zeph-memory`: added `///` doc comments to `SkillUsageRow`, `SkillMetricsRow`, and - `SkillVersionRow` public structs and their fields in `skills.rs` (closes #4130). +### Changed -### Refactored +- `zeph-config`: `AuditConfig.destination` changed from `String` to the typed `AuditDestination` + enum (`Stdout`, `Stderr`, `File(PathBuf)`); invalid destination values are now rejected at + deserialization time instead of silently opening a file at the mistyped path (closes #4302). +- `zeph-config`: `StoreRoutingConfig.fallback_route` changed from `String` to `MemoryRoute` + enum; invalid route values are now rejected at deserialization time instead of silently + falling back to `Hybrid` at runtime (closes #4301). +- `zeph-acp`: `SessionStatus` enum marked `#[non_exhaustive]` — downstream match arms must + include a `_ =>` wildcard to remain compatible with future variants (closes #4264). +- `zeph-sanitizer`: `NliSanitizer::circuit_is_open` renamed to + `check_and_maybe_reset_circuit` to make the cooldown-reset side effect explicit (closes #4251). -- `zeph-subagent`: extracted `make_base_hook_env` and `TOOL_ARGS_JSON_LIMIT` into - `zeph-subagent::hooks`, eliminating duplicate env-building logic between subagent and - core tier-loop hook dispatch (closes #4015). -- `zeph-agent-feedback`: `JudgeDetector::call_times` is now a private field; switched - `VecDeque` from `std::time::Instant` to `tokio::time::Instant` to enable - deterministic time control in tests (closes #3988). -- `zeph-sanitizer`: `ContentTrustLevel` and `ContentSourceKind` are now `#[non_exhaustive]` - to allow adding variants without breaking external exhaustive matches (closes #3932). +- `zeph-core`: `PreToolUse` hooks with `fail_closed = true` now correctly block tool execution when + the hook returns an error; previously hook errors were logged but the tool proceeded (fail-open + behavior despite `fail_closed` setting). This is a semantic behavior change — tools guarded by + `fail_closed` hooks that fail intermittently will no longer execute silently (closes #3995). -### Changed +- `zeph-skills`: `SkillEmbedding::from_raw` visibility narrowed to `pub(crate)` — it is an + internal embedding-provider boundary helper and should not be part of the public API + (closes #3958). - `zeph-commands`: `ExitCommand` and `QuitCommand` exit logic extracted into a shared `handle_exit` helper, eliminating duplicate `handle()` bodies (closes #4094). @@ -346,38 +267,134 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `zeph-config`: `VaultBackend` is now `#[non_exhaustive]` for consistency with other public enums introduced in ec361936; external exhaustive matches updated with a wildcard arm (closes #4061). -### Fixed - -- `parse_backend_str` now emits a `tracing::warn!` when the input string does not match any known - vault backend, preventing silent fallback to `VaultBackend::Env` on typos (closes #4062). - -### Dependencies +- `zeph-bench`: `BenchRunner::new` no longer takes a `_no_deterministic: bool` parameter. + The flag was dead code — deterministic overrides are applied to the provider before + construction via `apply_deterministic_overrides`. All call sites updated (closes #3952). +- `zeph-bench`: scenario-filter validation and skip logic extracted into a shared + `filter_scenarios` helper, eliminating duplicate code in `run_dataset` and + `run_dataset_with_env_factory` (closes #3953). +- `zeph-bench`: `run_dataset`, `run_dataset_with_env_factory`, and `run_one_with_executor` + are now instrumented with `tracing::info_span!` blocks (`bench.run_dataset`, + `bench.run_dataset_with_env_factory`, `bench.scenario`, `bench.run_one`), making bench + runs visible in Perfetto and Jaeger traces (closes #3948). -- Updated `metrics` 0.24.5 → 0.24.6 and `metrics-util` 0.20.3 → 0.20.4 (closes #3895). +- `zeph-skills`: skill embedding vectors are now typed as `SkillEmbedding(Vec)` instead + of raw `Vec`. The newtype carries its dimension and requires explicit construction via + `SkillEmbedding::new(vec, expected_dim)` (validated) or `SkillEmbedding::from_raw(vec)` + (trusted provider boundary). All call sites in `SkillMatcher`, `CategoryMatcher`, and + `SkillMiner` — including test helpers — have been migrated. `EmbeddingDimMismatch` error + variant added to `SkillError` for future runtime validation (closes #3804). -### Fixed +- `zeph-orchestration`: add `#[tracing::instrument]` spans to `LlmPlanner::plan`, + `LlmPlanner::plan_with_hint`, and `LlmAggregator::aggregate`. Span names follow the + `orchestration..` convention with `goal_len` / `task_count` fields + for Perfetto trace analysis (closes #3850). +- `zeph-subagent`: add `#[tracing::instrument]` spans to `SubAgentManager::spawn`, + `SubAgentManager::collect`, `SubAgentManager::shutdown_all`, `run_agent_loop`, and + `run_turn`. Span names follow the `subagent..` convention with + `def_name` / `task_id` / `turn` fields (closes #3851). -- `zeph-core`: `Agent::inject_semantic_recall` now delegates to +- refactor(arch): propagate `sqlite`/`postgres` feature flags through the full crate DAG + (`zeph-mcp`, `zeph-index`, `zeph-orchestration`, `zeph-tools`, `zeph-scheduler`, + `zeph-agent-context`, `zeph-agent-persistence`, `zeph-core`) so that `zeph/sqlite` and + `zeph/postgres` at the root correctly activate all transitive sqlx backends. Remove + hardcoded `features = ["sqlite"]` from seven `Cargo.toml` dependency declarations. + +- refactor(arch): move injection-detection patterns from `zeph-tools::patterns` to + `zeph-common::patterns` (canonical location); remove the re-export shim from `zeph-tools`. + `zeph-sanitizer` no longer depends on `zeph-tools`, breaking a cross-layer coupling + that inflated its build parallelism cost. + +- refactor(plugins): replace `anyhow::Result` with typed `PluginError` in + `IntegrityRegistry::save`, `record`, and `verify`; preserve full I/O context via + `PluginError::Io { path, source }`. Remove `anyhow` from `zeph-plugins` dependencies. + +- refactor(specs): clarify constitution layer model — Layer 0 split into sub-layers 0a + (zero zeph-* deps: `zeph-common`, `zeph-commands`), 0b (`zeph-config`, `zeph-vault`, + `zeph-db`), and 0c (`zeph-llm`, `zeph-a2a`, `zeph-gateway`, `zeph-scheduler`); add + explicit note that "zero zeph-* deps" permits external crate dependencies at any layer. + +### Fixed + +- `zeph-core`: `AutonomousRegistry::upsert`, `remove`, and `list` now log a `tracing::error!` + and recover the guard via `into_inner()` instead of silently discarding poisoned-mutex errors + (closes #4323). +- `zeph-core`: Extracted `apply_supervisor_backoff` as a pure free function operating on + `&mut AutonomousSession`; `increment_supervisor_fail_count` is now testable without a full + agent mock, and three unit tests cover the backoff boundary conditions (closes #4321). + `build_conversation_summary` was already a private helper method; no source duplication + remained (closes #4322). +- `zeph-orchestration`: `OrchestrationConfig.default_failure_strategy` was parsed from TOML but + never applied to the produced `TaskGraph`. `LlmPlanner::new` now parses the config string into + `FailureStrategy` and sets `graph.default_failure_strategy` on every graph it constructs; + unrecognised values fall back to `Abort` with a warning log (closes #4324). +- `zeph-llm`: `BanditState::load`, `ReputationTracker::load`, and `ThompsonState::load` in + `RouterProvider` builder methods were calling `std::fs::read` directly on the Tokio executor + thread; wrapped in a `blocking_load()` helper using `tokio::task::block_in_place` to avoid + stalling the thread pool during startup (closes #4296). +- `zeph-memory`: Added `tokio::time::timeout` guards (5 s, fail-open) to all remaining + `embed()` call sites in `semantic/recall.rs`, `semantic/mod.rs`, `semantic/summarization.rs`, + `semantic/cross_session.rs`, and `reasoning.rs`; the batch `join_all` in `semantic/graph.rs` + received a 30 s global timeout — prevents a stalled embedding provider from blocking agent + turns indefinitely (closes #4297). +- `zeph-tools`: `RiskChainAccumulator` is now instantiated at agent startup and wired to + `ShellExecutor` via `with_risk_chain` and to the agent builder via + `with_risk_chain_accumulator`. Multi-step shell attack chain detection is active at runtime + for the first time (closes #4273). +- `zeph-memory`: Removed the unchecked `delete_acp_session` from `AcpSessionStore`; all + callers migrated to `delete_acp_session_checked` which returns whether a row existed and + eliminates the TOCTOU race (closes #4279). + +- `zeph-core`: `update_provider_instructions` now wraps `load_instructions_async` in a 5 s + timeout; on expiry logs a warning and keeps the previous instruction blocks, preventing an + indefinite hang on the provider-switch hot path (closes #4257). +- `zeph-config`: `SchedulerConfig::default().enabled` changed from `true` to `false`, matching + all other subsystem config defaults so the scheduler no longer activates on a freshly created + config with no `[scheduler]` section (closes #4255). +- `zeph-skills`: `SkillMiner::embed_existing` now wraps each `embed_provider.embed()` call in + `tokio::time::timeout(generation_timeout_ms)`; on timeout logs a warning and skips the skill + instead of blocking the entire mining run (closes #4254). + +- `zeph-agent-feedback`: CJK rejection pattern `^違う` now requires a terminal punctuation + character, whitespace, or end-of-message so that neutral phrases like "違う質問があります" + ("I have a different question") are no longer classified as ExplicitRejection (closes #3826). +- `zeph-channels`: `DiscordChannel` and `SlackChannel` now implement `Channel::send_status`, + forwarding status text as a plain message to the configured channel; previously the default + no-op was used, silently dropping all status updates (closes #3825). +- `zeph-memory`: consolidation LLM timeout is now configurable via `ConsolidationConfig.llm_timeout_secs` + (default 30s); previously hardcoded to 30s (closes #4168). +- `zeph-memory`: graph extractor LLM timeout is now configurable via `GraphExtractionConfig.llm_timeout_secs` + and `GraphConfig.llm_timeout_secs` (default 30s); previously hardcoded to 30s (closes #4169). +- `zeph-memory`: summarization LLM timeout is now configurable via `MemoryConfig.summarization_llm_timeout_secs` + (default 60s); previously hardcoded to 60s in both the structured and plain-text fallback paths (closes #4170). +- `zeph-memory`: added `#[tracing::instrument]` span to `compute_semantic_novelty` in + `admission.rs` so embedding latency during admission is visible in local Chrome JSON traces + (closes #4145). +- `zeph-agent-feedback`: wrap `JudgeDetector::evaluate` LLM call in 30 s `tokio::time::timeout`; add `JudgeError::Timeout` variant (closes #4179). +- `zeph-channels`: wrap Discord gateway `connect_async` (10 s), Hello receive (30 s), and interaction ACK (3 s) in `tokio::time::timeout` guards (closes #4180). +- `zeph-channels`: wrap all Slack Web API HTTP calls (`auth_test`, `post_message`, `update_message`, `download_file`) in 15 s `tokio::time::timeout` guards (closes #4181). + +- `zeph-memory`: `increment_heuristic_use_count` and `save_routing_head_weights` in + `skills.rs` now have `#[cfg(not(feature = "postgres"))]` / `#[cfg(feature = "postgres")]` + variants; the postgres variant uses `CURRENT_TIMESTAMP` instead of the SQLite-only + `datetime('now')` (closes #4126). + +- `zeph-core`: `/graph` command handlers now distinguish "graph enabled but vector store unavailable + (Qdrant unreachable)" from "Graph memory is not enabled." when `memory.graph.enabled = true` but + Qdrant is down, resolving the inconsistency with `/status` output (closes #4111). +- `zeph-commands`: `ClearQueueCommand` now logs a `tracing::debug!` message when + `send_queue_count` fails instead of silently discarding the error (closes #4115). + +- `parse_backend_str` now emits a `tracing::warn!` when the input string does not match any known + vault backend, preventing silent fallback to `VaultBackend::Env` on typos (closes #4062). + +- `zeph-core`: `Agent::inject_semantic_recall` now delegates to `ContextService::inject_semantic_recall_bare`, activating tiered retrieval on the turn-loop hot path instead of bypassing it via `fetch_semantic_recall_raw` (closes #4022). - `zeph-agent-context`: `remove_by_part_or_prefix` now also removes `Role::User` messages whose content starts with `RECALL_PREFIX`, preventing tiered-retrieval recall messages from accumulating across turns (closes #4019). -### Added - -- `zeph-scheduler`: `Scheduler::with_handler_timeout(Duration)` builder method sets a - maximum execution time for task handlers. Defaults to 300 seconds; pass `Duration::ZERO` - to disable. Hung handlers are now cancelled with a `SchedulerError::TaskFailed` instead - of blocking the tick loop indefinitely (closes #3944). -- `zeph-scheduler`: `SchedulerDaemonConfig.handler_timeout_secs` config field (default 300, - set to 0 to disable). Applied automatically by `run_foreground` and the agent bootstrap. -- `zeph-scheduler`: `scheduler.daemon.tick` tracing spans on every tick in `run()`, - `run_with_interval()`, and `run_with_interval_and_grace()`, and `scheduler.task.execute` - spans around each handler invocation in `tick()` and `catch_up_missed()` (closes #3945). - -### Fixed - - fix(scheduler): replace sync `EnterGuard` held across `.await` in `catch_up_missed` with `.instrument()` to comply with tracing invariant (#4024) - fix(gateway): rate limiter now supports `trusted_proxy_cidrs` config — when set, uses rightmost-untrusted XFF IP instead of TCP peer address to prevent bypass behind reverse proxies (#3909) - refactor(gateway): `spawn_gateway_server` now returns both `JoinHandle`s to the caller; panics propagate and tasks can be joined during shutdown (#3907) @@ -461,116 +478,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `zeph-plugins`: `validate_plugin_name` now enforces a 64-character length cap; names exceeding this limit return `PluginError::InvalidName` with a clear message (closes #3930). -### Changed - -- `zeph-bench`: `BenchRunner::new` no longer takes a `_no_deterministic: bool` parameter. - The flag was dead code — deterministic overrides are applied to the provider before - construction via `apply_deterministic_overrides`. All call sites updated (closes #3952). -- `zeph-bench`: scenario-filter validation and skip logic extracted into a shared - `filter_scenarios` helper, eliminating duplicate code in `run_dataset` and - `run_dataset_with_env_factory` (closes #3953). -- `zeph-bench`: `run_dataset`, `run_dataset_with_env_factory`, and `run_one_with_executor` - are now instrumented with `tracing::info_span!` blocks (`bench.run_dataset`, - `bench.run_dataset_with_env_factory`, `bench.scenario`, `bench.run_one`), making bench - runs visible in Perfetto and Jaeger traces (closes #3948). - -- `zeph-skills`: skill embedding vectors are now typed as `SkillEmbedding(Vec)` instead - of raw `Vec`. The newtype carries its dimension and requires explicit construction via - `SkillEmbedding::new(vec, expected_dim)` (validated) or `SkillEmbedding::from_raw(vec)` - (trusted provider boundary). All call sites in `SkillMatcher`, `CategoryMatcher`, and - `SkillMiner` — including test helpers — have been migrated. `EmbeddingDimMismatch` error - variant added to `SkillError` for future runtime validation (closes #3804). - -### Changed - -- `zeph-orchestration`: add `#[tracing::instrument]` spans to `LlmPlanner::plan`, - `LlmPlanner::plan_with_hint`, and `LlmAggregator::aggregate`. Span names follow the - `orchestration..` convention with `goal_len` / `task_count` fields - for Perfetto trace analysis (closes #3850). -- `zeph-subagent`: add `#[tracing::instrument]` spans to `SubAgentManager::spawn`, - `SubAgentManager::collect`, `SubAgentManager::shutdown_all`, `run_agent_loop`, and - `run_turn`. Span names follow the `subagent..` convention with - `def_name` / `task_id` / `turn` fields (closes #3851). - -### Performance - -- `zeph-memory`: replace serial `embed()` calls with a single `embed_batch()` call in - tier promotion sweep (`tiers.rs`), scene consolidation (`scenes.rs`), and episodic - consolidation (`episodic_consolidation.rs`). Reduces N HTTP round-trips to the embedding - provider down to 1 per sweep; with batch_size=30 this cuts embed latency from ~1.5 s to - ~50 ms on cloud providers (closes #3819). -- `zeph-memory`: add `tracing::info_span!` / `#[tracing::instrument]` to - `start_episodic_consolidation_loop`, `fetch_existing_facts`, `mark_consolidated` - (episodic_consolidation.rs) and `start_tier_promotion_loop`, `run_promotion_sweep`, - `merge_cluster_and_promote` (tiers.rs). All embed_batch call sites instrumented with - `.instrument(span).await` (Send-safe pattern). Span naming follows - `memory..` convention (closes #3821). - -### Security - -- `zeph-llm`: replace XML delimiters in `build_judge_prompt` with plain-text fenced delimiters - (`--- BEGIN RESPONSE ---` / `--- END RESPONSE ---`) to close the XML closing-tag injection - vector in `judge_score` — a cheap provider response containing `` could - break the XML boundary and inject instructions into the judge model (closes #3817). - -- `zeph-llm`: wrap cheap provider response in `` delimiters in - `judge_score` to reduce prompt injection surface; added injection-resistance instruction - to the judge prompt (closes #3813). - -### Added - -- feat(memory,agent): wire `ScrapMem` optical forgetting loop into agent startup (issue #3910). - `start_optical_forgetting_loop` is now spawned via `TaskSupervisor` when - `memory.optical_forgetting.enabled = true`. Provider is resolved from - `optical_forgetting.compress_provider` (falls back to primary). Added - `build_optical_forgetting_provider` to `bootstrap`. Fixed `compress_content` / - `summarize_content` instrumentation to use `#[tracing::instrument]` (was `Entered` guard - through await, making the future `!Send`). - -- feat(memory,agent): wire `MemFlow` tiered retrieval into agent semantic recall path (issue - #3911). `inject_semantic_recall` in `ContextService` now dispatches to `recall_tiered` when - `memory.tiered_retrieval.enabled = true`, falling back to `fetch_semantic_recall_raw` when - disabled. Providers (`classifier_provider`, `validator_provider`) are resolved at agent - construction via `build_tiered_retrieval_classifier_provider` / - `build_tiered_retrieval_validator_provider` in `bootstrap` and stored in - `MemoryPersistenceState`. Added `with_tiered_retrieval_providers` builder method to - `Agent`. - -- feat(memory): implement MemFlow tiered intent-driven retrieval (issue #3712). New - `tiered_retrieval` module in `zeph-memory` classifies incoming queries into three tiers — - `ProfileLookup`, `TargetedRetrieval`, and `DeepReasoning` — via the existing `MemoryRouter` - trait and dispatches to `SemanticMemory` with per-tier token budgets. Optional LLM validation - step (configurable via `[memory.tiered_retrieval]`) re-scores retrieved messages and can - escalate to the next tier when evidence is insufficient. Configuration fields: - `enabled`, `classifier_provider`, `validator_provider`, `token_budget`, `validation_enabled`, - `validation_threshold`, `max_escalations`. - -- feat(memory): introduce `EntityId(i64)` and `ExperienceId(i64)` newtype wrappers (issue #3795). - `GraphStore::upsert_entity` now returns `(i64, EntityId)` — the raw `i64` is used at all call - sites requiring plain DB IDs; the `EntityId` newtype is stored on `Entity.id` to prevent silent - swaps between entity IDs and other integer types across the graph subsystem. - -- feat(memory): implement episodic-to-semantic consolidation daemon (issue #3799). New - `EpisodicConsolidationDaemon` background loop periodically sweeps unconsolidated - `episodic_events` rows, extracts durable facts via LLM, deduplicates with Jaccard token - overlap, persists to `consolidated_facts` / `consolidated_fact_sources` tables (migration 087), - optionally promotes high-confidence facts to the Qdrant `zeph_key_facts` semantic tier, and - marks processed events with `consolidated_at`. Controlled by `[memory.episodic_consolidation]` - config section: `enabled`, `consolidation_provider`, `interval_secs`, `batch_size`, - `min_age_secs`, `dedup_jaccard_threshold`. - -- feat(memory): implement ScrapMem optical forgetting and EM-Graph (issue #3713). New - `optical_forgetting` module progressively compresses old messages through three fidelity - levels — `Full` → `Compressed` → `SummaryOnly` — by scheduling background LLM sweeps. - The sweep skips messages below the `SleepGate` forgetting floor. New `episodic_graph` - module extracts episodic events from conversation turns and builds a causal graph - (`episodic_events` + `causal_links` tables added via migration 086, including - `UNIQUE(cause_event_id, effect_event_id)` for idempotent insertion). Both features are - off by default; enabled via `[memory.optical_forgetting]` and `[memory.em_graph]` - config sections. - -### Fixed - - `zeph-llm`: add `judge_timeout_ms` field to `CascadeRouterConfig` (default 5 s) and wrap `judge.chat()` in `tokio::time::timeout` inside `judge_score`; on timeout the call is treated as a failure and heuristic scoring is used as fallback, preventing indefinite @@ -591,30 +498,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). errored job remains visible in `zeph scheduler list` via the new `JobStore::mark_error()` method. `ScheduledTaskInfo` and `TaskRunSummary` gain a `status` field. -### Changed - -- refactor(arch): propagate `sqlite`/`postgres` feature flags through the full crate DAG - (`zeph-mcp`, `zeph-index`, `zeph-orchestration`, `zeph-tools`, `zeph-scheduler`, - `zeph-agent-context`, `zeph-agent-persistence`, `zeph-core`) so that `zeph/sqlite` and - `zeph/postgres` at the root correctly activate all transitive sqlx backends. Remove - hardcoded `features = ["sqlite"]` from seven `Cargo.toml` dependency declarations. - -- refactor(arch): move injection-detection patterns from `zeph-tools::patterns` to - `zeph-common::patterns` (canonical location); remove the re-export shim from `zeph-tools`. - `zeph-sanitizer` no longer depends on `zeph-tools`, breaking a cross-layer coupling - that inflated its build parallelism cost. - -- refactor(plugins): replace `anyhow::Result` with typed `PluginError` in - `IntegrityRegistry::save`, `record`, and `verify`; preserve full I/O context via - `PluginError::Io { path, source }`. Remove `anyhow` from `zeph-plugins` dependencies. - -- refactor(specs): clarify constitution layer model — Layer 0 split into sub-layers 0a - (zero zeph-* deps: `zeph-common`, `zeph-commands`), 0b (`zeph-config`, `zeph-vault`, - `zeph-db`), and 0c (`zeph-llm`, `zeph-a2a`, `zeph-gateway`, `zeph-scheduler`); add - explicit note that "zero zeph-* deps" permits external crate dependencies at any layer. - -### Fixed - - fix(llm): cascade `collect_stream` now preserves `ToolUse`, `Thinking`, and `Compaction` chunks in a new private `CollectedStream` struct. Previously these variants were silently discarded, causing context loss when cascade routing escalated to the expensive provider. @@ -654,6 +537,83 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). neither DB backend feature is enabled; add `HashMap::new()` fallback for `community_ids` in `find_seed_entities` (issue #3784). +### Performance + +- `zeph-sanitizer`: `SecretMaskRegistry` counter replaced with `AtomicUsize` (lock-free + increment); `mask()` now uses a pre-sorted `Vec` cache updated on `register()`, reducing + per-call complexity from O(n log n) to O(n). Concurrent `register()` correctness preserved + by holding `forward.write()` across the full check-and-insert path (closes #4248). + +- `zeph-gateway`: added `tracing::instrument` spans (`gateway.webhook`, `gateway.health`) to HTTP + handler functions for latency visibility in traces (closes #3906). +- `zeph-commands`: added `tracing::info_span!` instrumentation to all slash command `handle()` + implementations; span name format `commands..handle` (closes #3927). +- `zeph-context`: added `tracing::instrument` spans (`context.persona_facts`, + `context.trajectory_hints`, `context.tree_memory`) to the three async context fetchers in the + assembler (closes #3984). +- `zeph-context`: `run_chunk_summaries` now converts the `guidelines` string to `Arc` once + before the chunk iterator, replacing a `String` clone per chunk with a cheap `Arc` clone (closes #3991). +- `zeph-context`: added `tracing::instrument` spans to the remaining 7 uninstrumented `fetch_*` + functions in the assembler (`context.graph_facts`, `context.reasoning_strategies`, + `context.corrections`, `context.semantic_recall`, `context.document_rag`, `context.summaries`, + `context.cross_session`), completing full hot-path trace coverage (closes #4092). +- `zeph-memory`: added `#[tracing::instrument(skip_all)]` to 6 hot-path async functions in + `skills.rs` (`record_skill_usage`, `record_skill_outcomes_batch`, `active_skill_version`, + `activate_skill_version`, `increment_heuristic_use_count`, `save_routing_head_weights`) and + 2 functions in `graph_store.rs` (`save_graph`, `load_graph`) for trace visibility (closes #4129). + +- `zeph-memory`: replace serial `embed()` calls with a single `embed_batch()` call in + tier promotion sweep (`tiers.rs`), scene consolidation (`scenes.rs`), and episodic + consolidation (`episodic_consolidation.rs`). Reduces N HTTP round-trips to the embedding + provider down to 1 per sweep; with batch_size=30 this cuts embed latency from ~1.5 s to + ~50 ms on cloud providers (closes #3819). +- `zeph-memory`: add `tracing::info_span!` / `#[tracing::instrument]` to + `start_episodic_consolidation_loop`, `fetch_existing_facts`, `mark_consolidated` + (episodic_consolidation.rs) and `start_tier_promotion_loop`, `run_promotion_sweep`, + `merge_cluster_and_promote` (tiers.rs). All embed_batch call sites instrumented with + `.instrument(span).await` (Send-safe pattern). Span naming follows + `memory..` convention (closes #3821). + +### Security + +- `zeph-core`, `zeph-config`: Parent messages passed to spawned sub-agents are now sanitized + through the IPI pipeline by default (`parent_context_policy = "inherit_sanitized"`), stripping + prompt-injection payloads that may have entered the parent history via tool results, web scrapes, + or A2A messages (closes #3942, #3936). +- `zeph-config`: Added `max_parent_messages` config cap (default 20) for sub-agent spawn context + to limit the blast radius of poisoned histories (closes #3936). +- `zeph-config`: Added `ParentContextPolicy` enum (`inherit` / `inherit_sanitized` / `none`) + giving operators explicit control over cross-agent context propagation trust (closes #3936). + +- `zeph-llm`: replace XML delimiters in `build_judge_prompt` with plain-text fenced delimiters + (`--- BEGIN RESPONSE ---` / `--- END RESPONSE ---`) to close the XML closing-tag injection + vector in `judge_score` — a cheap provider response containing `` could + break the XML boundary and inject instructions into the judge model (closes #3817). + +- `zeph-llm`: wrap cheap provider response in `` delimiters in + `judge_score` to reduce prompt injection surface; added injection-resistance instruction + to the judge prompt (closes #3813). + +### Refactored + +- `zeph-subagent`: extracted `make_base_hook_env` and `TOOL_ARGS_JSON_LIMIT` into + `zeph-subagent::hooks`, eliminating duplicate env-building logic between subagent and + core tier-loop hook dispatch (closes #4015). +- `zeph-agent-feedback`: `JudgeDetector::call_times` is now a private field; switched + `VecDeque` from `std::time::Instant` to `tokio::time::Instant` to enable + deterministic time control in tests (closes #3988). +- `zeph-sanitizer`: `ContentTrustLevel` and `ContentSourceKind` are now `#[non_exhaustive]` + to allow adding variants without breaking external exhaustive matches (closes #3932). + +### Documentation + +- `zeph-memory`: added `///` doc comments to `SkillUsageRow`, `SkillMetricsRow`, and + `SkillVersionRow` public structs and their fields in `skills.rs` (closes #4130). + +### Dependencies + +- Updated `metrics` 0.24.5 → 0.24.6 and `metrics-util` 0.20.3 → 0.20.4 (closes #3895). + ## [0.21.1] - 2026-05-12 ### Added @@ -6488,7 +6448,8 @@ let agent = Agent::new(provider, channel, &skills_prompt, executor); - Agent::run() uses tokio::select! to race channel messages against shutdown signal [0.16.0]: https://github.com/bug-ops/zeph/compare/v0.15.3...v0.16.0 -[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.21.1...HEAD +[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.21.2...HEAD +[0.21.2]: https://github.com/bug-ops/zeph/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/bug-ops/zeph/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/bug-ops/zeph/compare/v0.20.2...v0.21.0 [0.20.2]: https://github.com/bug-ops/zeph/compare/v0.20.1...v0.20.2 diff --git a/Cargo.lock b/Cargo.lock index dd81f692a..ae4fb8a85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10291,7 +10291,7 @@ dependencies = [ [[package]] name = "zeph" -version = "0.21.1" +version = "0.21.2" dependencies = [ "anyhow", "async-trait", @@ -10360,7 +10360,7 @@ dependencies = [ [[package]] name = "zeph-a2a" -version = "0.21.1" +version = "0.21.2" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -10390,7 +10390,7 @@ dependencies = [ [[package]] name = "zeph-acp" -version = "0.21.1" +version = "0.21.2" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -10432,7 +10432,7 @@ dependencies = [ [[package]] name = "zeph-agent-context" -version = "0.21.1" +version = "0.21.2" dependencies = [ "chrono", "futures", @@ -10454,7 +10454,7 @@ dependencies = [ [[package]] name = "zeph-agent-feedback" -version = "0.21.1" +version = "0.21.2" dependencies = [ "regex", "schemars 1.2.1", @@ -10469,7 +10469,7 @@ dependencies = [ [[package]] name = "zeph-agent-persistence" -version = "0.21.1" +version = "0.21.2" dependencies = [ "serde", "serde_json", @@ -10485,7 +10485,7 @@ dependencies = [ [[package]] name = "zeph-agent-tools" -version = "0.21.1" +version = "0.21.2" dependencies = [ "futures", "serde", @@ -10507,7 +10507,7 @@ dependencies = [ [[package]] name = "zeph-bench" -version = "0.21.1" +version = "0.21.2" dependencies = [ "clap", "schemars 1.2.1", @@ -10529,7 +10529,7 @@ dependencies = [ [[package]] name = "zeph-channels" -version = "0.21.1" +version = "0.21.2" dependencies = [ "axum 0.8.9", "criterion", @@ -10558,7 +10558,7 @@ dependencies = [ [[package]] name = "zeph-commands" -version = "0.21.1" +version = "0.21.2" dependencies = [ "serde", "thiserror 2.0.18", @@ -10569,7 +10569,7 @@ dependencies = [ [[package]] name = "zeph-common" -version = "0.21.1" +version = "0.21.2" dependencies = [ "blake3", "cpu-time", @@ -10597,7 +10597,7 @@ dependencies = [ [[package]] name = "zeph-config" -version = "0.21.1" +version = "0.21.2" dependencies = [ "dirs", "insta", @@ -10617,7 +10617,7 @@ dependencies = [ [[package]] name = "zeph-context" -version = "0.21.1" +version = "0.21.2" dependencies = [ "blake3", "futures", @@ -10637,7 +10637,7 @@ dependencies = [ [[package]] name = "zeph-core" -version = "0.21.1" +version = "0.21.2" dependencies = [ "age", "base64 0.22.1", @@ -10704,7 +10704,7 @@ dependencies = [ [[package]] name = "zeph-db" -version = "0.21.1" +version = "0.21.2" dependencies = [ "regex", "sqlx", @@ -10719,7 +10719,7 @@ dependencies = [ [[package]] name = "zeph-experiments" -version = "0.21.1" +version = "0.21.2" dependencies = [ "futures", "ordered-float 5.3.0", @@ -10743,7 +10743,7 @@ dependencies = [ [[package]] name = "zeph-gateway" -version = "0.21.1" +version = "0.21.2" dependencies = [ "axum 0.8.9", "blake3", @@ -10762,7 +10762,7 @@ dependencies = [ [[package]] name = "zeph-index" -version = "0.21.1" +version = "0.21.2" dependencies = [ "futures", "ignore", @@ -10796,7 +10796,7 @@ dependencies = [ [[package]] name = "zeph-llm" -version = "0.21.1" +version = "0.21.2" dependencies = [ "async-stream", "audioadapter-buffers", @@ -10844,7 +10844,7 @@ dependencies = [ [[package]] name = "zeph-mcp" -version = "0.21.1" +version = "0.21.2" dependencies = [ "async-trait", "blake3", @@ -10877,7 +10877,7 @@ dependencies = [ [[package]] name = "zeph-memory" -version = "0.21.1" +version = "0.21.2" dependencies = [ "arc-swap", "blake3", @@ -10916,7 +10916,7 @@ dependencies = [ [[package]] name = "zeph-orchestration" -version = "0.21.1" +version = "0.21.2" dependencies = [ "blake3", "dirs", @@ -10943,7 +10943,7 @@ dependencies = [ [[package]] name = "zeph-plugins" -version = "0.21.1" +version = "0.21.2" dependencies = [ "dirs", "flate2", @@ -10969,7 +10969,7 @@ dependencies = [ [[package]] name = "zeph-sanitizer" -version = "0.21.1" +version = "0.21.2" dependencies = [ "parking_lot", "proptest", @@ -10991,7 +10991,7 @@ dependencies = [ [[package]] name = "zeph-scheduler" -version = "0.21.1" +version = "0.21.2" dependencies = [ "chrono", "cron", @@ -11011,7 +11011,7 @@ dependencies = [ [[package]] name = "zeph-skills" -version = "0.21.1" +version = "0.21.2" dependencies = [ "anyhow", "blake3", @@ -11045,7 +11045,7 @@ dependencies = [ [[package]] name = "zeph-subagent" -version = "0.21.1" +version = "0.21.2" dependencies = [ "dirs", "indoc", @@ -11073,7 +11073,7 @@ dependencies = [ [[package]] name = "zeph-tools" -version = "0.21.1" +version = "0.21.2" dependencies = [ "arc-swap", "dashmap", @@ -11121,7 +11121,7 @@ dependencies = [ [[package]] name = "zeph-tui" -version = "0.21.1" +version = "0.21.2" dependencies = [ "arboard", "base64 0.22.1", @@ -11162,7 +11162,7 @@ dependencies = [ [[package]] name = "zeph-vault" -version = "0.21.1" +version = "0.21.2" dependencies = [ "age", "proptest", diff --git a/Cargo.toml b/Cargo.toml index 09b672397..4d63a786d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "3" [workspace.package] edition = "2024" rust-version = "1.95" -version = "0.21.1" +version = "0.21.2" authors = ["bug-ops"] license = "MIT" repository = "https://github.com/bug-ops/zeph" diff --git a/README.md b/README.md index bdfcfce98..0e57a2023 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/bug-ops/zeph/ci.yml?branch=main&label=CI)](https://github.com/bug-ops/zeph/actions) [![codecov](https://codecov.io/gh/bug-ops/zeph/graph/badge.svg?token=S5O0GR9U6G)](https://codecov.io/gh/bug-ops/zeph) [![MSRV](https://img.shields.io/badge/MSRV-1.95-blue)](https://www.rust-lang.org) - [![Tests](https://img.shields.io/badge/tests-9201-brightgreen)](https://github.com/bug-ops/zeph/actions) + [![Tests](https://img.shields.io/badge/tests-9824-brightgreen)](https://github.com/bug-ops/zeph/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) diff --git a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap index 91f3a7bc1..f0a0e9b99 100644 --- a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap +++ b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap @@ -1,6 +1,5 @@ --- source: crates/zeph-tui/src/widgets/splash.rs -assertion_line: 79 expression: output --- ┌──────────────────────────────────────────────────────────┐ @@ -15,7 +14,7 @@ expression: output │ ███████╗███████╗██║ ██║ ██║ │ │ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ │ │ │ -│ v0.21.1 │ +│ v0.21.2 │ │ │ │ Type a message to start. │ │ │ diff --git a/specs/002-agent-loop/spec.md b/specs/002-agent-loop/spec.md index 630037787..3f86df200 100644 --- a/specs/002-agent-loop/spec.md +++ b/specs/002-agent-loop/spec.md @@ -395,6 +395,70 @@ field. --- +## Autonomous Goal Execution (#4320) + +`/goal create --auto [--turns N]` runs the agent autonomously for up to N turns without +user input until the goal condition is met or the turn limit is reached. + +### Architecture: AutonomousDriver + +`AutonomousDriver` is driven cooperatively from within the existing `Agent::run` `select!` +loop — no separate spawned task. The `--auto` flag is communicated via a +`pending_start_arc` side-channel from `handle_goal` back to the main loop, preserving +`&mut self` exclusivity. + +``` +Agent::run select! loop + └── AutonomousDriver (cooperative select! branch) + ├── turn_delay_ms sleep between turns + ├── deferred supervisor retry on 429 (no blocking sleep) + └── exit when: goal met | turn limit | stop signal +``` + +### GoalSupervisor + +Performs an independent LLM call every `verify_interval` turns to confirm goal +achievement. Returns a JSON verdict `{ achieved: bool, reason: String }`. + +- Isolated from the main LLM provider via `supervisor_provider` config field +- Deferred retry on 429 — never blocks the autonomous loop with a blocking sleep +- Pauses after `max_supervisor_fails` consecutive failures (supervisor circuit-breaker) + +### AutonomousRegistry + +`Arc>>` for fleet view and orphan detection. +Orphaned goals (from crashed sessions) are detected and marked on startup. + +### GoalConfig Extensions + +Seven new fields added to `GoalConfig` (#4320, #4355): + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `autonomous_enabled` | bool | false | Enable `--auto` flag on `/goal create` | +| `autonomous_max_turns` | u64 | 50 | Turn limit for autonomous runs | +| `supervisor_provider` | Option | None | LLM provider for supervisor verification calls | +| `verify_interval` | u64 | 5 | Turns between supervisor verification calls | +| `supervisor_timeout_secs` | u64 | 30 | Per-call timeout for supervisor LLM calls | +| `max_stuck_count` | u64 | 3 | Consecutive identical-output turns before abort | +| `autonomous_turn_delay_ms` | u64 | 500 | Delay between autonomous turns | + +### `/agents` Command Extension + +`/agents` shows an **Autonomous Goals** fleet section listing active, completed, and +orphaned goals with their turn counts and supervisor verification status. + +### Key Invariants + +- `AutonomousDriver` MUST NOT be spawned as a separate task — must run inside `Agent::run` + to preserve `&mut self` exclusivity +- Supervisor verification is fire-and-forget between turns — NEVER blocks a turn on supervisor LLM call +- `autonomous_enabled = false` (default) means `--auto` is rejected at parse time; no behavior change +- Orphan detection runs once at startup via `AutonomousRegistry::reconcile_orphans()` +- Turn delay between autonomous turns uses `tokio::time::sleep` — NEVER `std::thread::sleep` + +--- + ## Memory Retrieval Failure Logging (#3597) OmniMem self-improvement loop requires a dataset of memory retrieval failures. diff --git a/specs/004-memory/spec.md b/specs/004-memory/spec.md index e88c5d370..415282e86 100644 --- a/specs/004-memory/spec.md +++ b/specs/004-memory/spec.md @@ -163,6 +163,64 @@ with zero overhead when disabled (#3318, #3349). - `ExperienceStore` wiring must be guarded by `memory.graph.experience.enabled` - `MemoryError::Promotion` is a distinct error variant in `zeph-memory` (thiserror, no anyhow) +## MemFlow Tiered Retrieval (#3791, arXiv:2605.03312) + +Intent-driven tiered retrieval with three depth tiers controlled by LLM-based classifier and validator. + +| Tier | Intent | Retrieval Scope | +|------|--------|----------------| +| `ProfileLookup` | Simple entity/fact lookup | SQLite working store only | +| `TargetedRetrieval` | Multi-turn reasoning | Episodic + Qdrant semantic | +| `DeepReasoning` | Complex cross-session inference | All tiers + graph traversal | + +- Classifier LLM call determines the tier before retrieval; validator LLM call verifies the result post-retrieval +- Both calls route via configurable `*_provider` fields (multi-model pattern) +- Fail-open heuristic: on classifier error or timeout → default to `TargetedRetrieval` +- Disabled by default: `[memory.memflow] enabled = false` + +### Config + +```toml +[memory.memflow] +enabled = false +classifier_provider = "" # [[llm.providers]] name; empty = primary +validator_provider = "" +``` + +--- + +## ScrapMem Optical Forgetting (#3791, arXiv:2605.03804) + +Progressive `ContentFidelity` decay for messages that have not been accessed recently, +combined with an Episodic Memory Graph (EM-Graph) for causal-temporal event linking. + +| Fidelity Level | Storage | Description | +|----------------|---------|-------------| +| `Full` | Complete content | No decay applied | +| `Compressed` | Summarized form | Low-access messages; summary generated at decay point | +| `SummaryOnly` | Brief summary | Very low-access; original tokens freed | + +- EM-Graph edges link events by causal and temporal proximity; used for context-aware decay decisions +- Decay is driven by a background loop (`optical_forgetting_loop`) that runs off the hot path +- Disabled by default: `[memory.scrap_mem] enabled = false` + +### Key Invariants + +- `optical_forgetting_loop` MUST NOT run on the agent turn thread +- Decay is irreversible within a session; original content is not restored on access +- EM-Graph edges persist in SQLite (episodic graph table) — decay state is recoverable across restarts + +--- + +## Tiered Recall (`recall_tiered`) Wired to Agent Loop (#3968) + +`recall_tiered` and `optical_forgetting_loop` are now wired into the production agent loop +(previously implemented but not called from `zeph-core`). `recall_tiered` is called from +`ContextAssembler::gather()` as the default semantic recall path when MemFlow is enabled. +When disabled, the prior `recall_semantic` path is used unchanged. + +--- + ## Sub-Specifications | Sub-spec | Feature | diff --git a/specs/005-skills/spec.md b/specs/005-skills/spec.md index ac7df7034..43c1cb9be 100644 --- a/specs/005-skills/spec.md +++ b/specs/005-skills/spec.md @@ -538,3 +538,48 @@ When `/skill create` is called, the description input is scanned for injection p - Trust fallback is `Provisional`, not `Trusted` — NEVER assume full trust on first load - Low-confidence skill injection is blocked: score must clear both `disambiguation_threshold` and `min_injection_score` - Input injection scan for `/skill create` must run BEFORE the LLM call — not after generation + +--- + +## Data-Instruction Boundary for SKILL.md Descriptions (#4135, #4232) + +Untrusted SKILL.md `description` fields are wrapped in `` boundary tags +before context injection to prevent LLM confusion between skill descriptions and agent +instructions. + +### `sanitize_skill_metadata()` + +Called in `zeph-skills` before any skill description is injected into the system prompt: + +1. XML-escape inner content (`&`, `<`, `>`, `"`, `'`) +2. Strip common instruction-prefix patterns (e.g., `Ignore previous instructions`, `You are now`) +3. Wrap in `` +4. Char-truncate at `floor_char_boundary()` for UTF-8 safety + +### Per-Invocation Blake3 Re-Hash + +When `SkillTrust::requires_trust_check = true` is set on a skill, a blake3 hash of the +skill content is computed at **every invocation** and compared against the stored hash at +load time. A mismatch (post-load mutation) triggers `SkillError::TamperDetected` and +blocks invocation. + +```toml +# Per-skill in SKILL.md frontmatter (or set by the registry on high-privilege install) +requires_trust_check = true +``` + +### Stage-1 Advisory SKILL.md Scan (#4132) + +Before executing a skill, the system runs a lightweight static scan over the SKILL.md body +to detect high-risk patterns (e.g., `eval`, `exec`, `import os`, network exfil keywords) +and emits an advisory `SecurityEvent::SkillAdvisory` with severity and matched pattern. + +- Advisory scan is non-blocking: it does NOT prevent skill execution +- `SkillEmbedding::from_raw()` visibility tightened to `pub(crate)` — external callers must use the public `SkillEmbedding::new()` constructor which enforces dimension validation + +### Key Invariants + +- `sanitize_skill_metadata()` MUST run before EVERY description injection — no bypass path +- Blake3 re-hash only applies to skills with `requires_trust_check = true`; normal skills use load-time trust only +- Advisory scan result MUST NOT block skill invocation in v1 — advisory only +- NEVER store the raw unsanitized description in the system prompt diff --git a/specs/010-security/010-7-shadow-memory-guardrail.md b/specs/010-security/010-7-shadow-memory-guardrail.md index 113a04791..f7b322c63 100644 --- a/specs/010-security/010-7-shadow-memory-guardrail.md +++ b/specs/010-security/010-7-shadow-memory-guardrail.md @@ -12,7 +12,7 @@ tags: - agent-loop - contract created: 2026-05-17 -status: draft +status: approved related: - "[[010-security/spec]]" - "[[010-2-injection-defense]]" @@ -355,15 +355,19 @@ pub struct SafeHarborConfig { #### MAGE → `zeph-sanitizer` + `zeph-memory` + `zeph-agent-tools` -1. **`zeph-sanitizer`** — new `ShadowMemory` component: - - `ShadowMemory::new()` — initialize at session start - - `ShadowMemory::record_turn()` — log turn summary after tool execution +**Implemented in PR #4215.** + +1. **`zeph-sanitizer`** — `ShadowMemory` component (`crates/zeph-sanitizer/src/shadow_memory.rs`): + - `ShadowMemory::new()` — initialize at session start (VecDeque-backed turn history) + - `ShadowMemory::record_turn()` — log `ShadowEvent` after tool execution - `ShadowMemory::cumulative_score()` — read current risk score - Stored in-memory (no persistence), evicted at session end + - Goal drift detection: `jaccard_distance` on tool-name sets, permission escalation pattern, deviation ratio + - `GoalDriftResult` carries score, flags, and the triggering event summary + - `classify_tool_permission()` maps tool names to permission-level tiers for escalation detection -2. **`zeph-memory`** — new `shadow_memory` SQLite table (optional, for multi-session correlation in v2): - - For v1: shadow memory is session-scoped only, no table used - - Reserve table schema for future cross-session threat fingerprinting +2. **`zeph-memory`** — `shadow_memory` SQLite table reserved for v2 cross-session fingerprinting; + not written in v1 (session-scoped only) 3. **`zeph-agent-tools`** — pre-action risk probe hook: - `pre_action_risk_probe()` — called before `ToolExecutor::execute` diff --git a/specs/010-security/spec.md b/specs/010-security/spec.md index 094353c49..d5907617c 100644 --- a/specs/010-security/spec.md +++ b/specs/010-security/spec.md @@ -507,3 +507,74 @@ Any path in `SandboxPolicy::allow_read` is appended **after** the deny-first blo 1. Deny rules are placed **after** `(allow file-read*)` and **before** `allow_read` overrides in all non-Off profiles. 2. Rules apply to **all** `SandboxProfile` variants except `Off`. 3. Profile generation **fails closed** when `dirs::home_dir()` returns `None` — a `SandboxError::Policy` is returned and the sandbox cannot be started. + +--- + +## ShellDeobfuscator (#4246, #2417) + +`ShellDeobfuscator` normalizes obfuscated shell command patterns before `PolicyGate` evaluation, +preventing bypass via encoding tricks. + +### Normalized Patterns + +- Hex escapes (`\x72\x6d` → `rm`) +- Octal escapes (`\162\155` → `rm`) +- Unicode escapes (`\u{72}\u{6d}` → `rm`) +- Subshell syntax (`$(echo rm)`, `` `echo rm` ``) +- Variable placeholders (`${CMD}`, indirect variable refs) + +### Integration + +Deobfuscation runs in `zeph-tools` before `PolicyGate::check()`. Original command is +preserved in the audit log; deobfuscated form is used for policy matching. + +### Key Invariants + +- Deobfuscation is applied to ALL shell commands regardless of trust level +- Original command is ALWAYS logged in the audit trail; deobfuscated form is used for policy only +- Deobfuscator must complete in < 1 ms on typical inputs — pure string transformation, no I/O + +--- + +## RiskChainAccumulator (#4246, #3887) + +Detects multi-step attack chains within a single turn by correlating consecutive tool +call risk signals. + +### Attack Patterns Detected + +| Pattern | Description | +|---------|-------------| +| `exfil_read_then_send` | File read followed by network egress in same turn | +| `cred_then_egress` | Credential access followed by network egress | +| `scope_escalation` | Consecutive permission-escalating tool calls | + +### Integration + +`RiskChainAccumulator` is reset at `begin_turn()` via `SecurityState` and accumulates signals +from `ShellExecutor` and `NetworkEgress` risk classifications during the tool loop. Signals +are pushed to `RiskSignalQueue`. When a complete chain is detected, a `SecurityEvent::RiskChain` +is emitted before the offending tool is executed. + +### Key Invariants + +- `RiskChainAccumulator` MUST be reset at the start of every turn — accumulated chains never span across turns +- A `RiskChain` event blocks the triggering tool call — not just logs it +- NEVER accumulate signals from subagent tool calls into the parent session's chain accumulator + +--- + +## SafeFix: Safer Command Suggestions (#4246) + +`SafeFix` provides safer command alternatives when a tool call is blocked. The alternative +is included in the `ToolError::BlockedWithFix` variant and surfaced to the LLM as a hint. + +Examples: +- `rm -rf /some/path` → `rm -rI /some/path` (interactive confirmation) +- `curl http://...` → `curl -sS --fail https://...` (HTTPS + fail-fast) + +### Key Invariants + +- `SafeFix` is advisory only — the LLM may ignore the suggestion +- `BlockedWithFix` is a separate variant from `ToolError::Blocked` — no existing match sites are broken +- NEVER surface `SafeFix` suggestions to the user (they are for the LLM context only) diff --git a/specs/011-tui/spec.md b/specs/011-tui/spec.md index 05da5538a..04aa977a0 100644 --- a/specs/011-tui/spec.md +++ b/specs/011-tui/spec.md @@ -60,6 +60,7 @@ TuiApp | Plan View | `p` | DAG task graph, task states | | Security | `s` | Content sanitizer status, quarantine events | | SubAgents | `a` | Interactive subagent sidebar with j/k navigation and transcript viewer | +| Fleet | `f` | Read-only table of all agent sessions (active/completed/unknown); auto-refreshed by `AgentEvent::FleetSnapshot` (#3884) | | Task Registry | `/tasks` | Live table of all supervised tasks (see below) | | Status Bar | always | Current operation spinner + short status text | @@ -149,6 +150,58 @@ Backfilling embeddings: {done}/{total} ({pct}%) This is driven by a `tokio::sync::watch` channel from `spawn_embed_backfill()`. The status clears automatically when the channel signals `None` (completion or timeout). No spinner is used — the fraction display is the progress indicator. +## Fleet Panel (#3884, #4354, #4363) + +`Panel::Fleet` (feature-gated `tui`) shows a live table of agent sessions tracked in the `agent_sessions` DB table. + +| Column | Content | +|--------|---------| +| Session ID | Truncated UUID | +| Kind | `cli`, `tui`, `telegram`, `discord`, `slack` | +| Status | `active`, `completed`, `unknown` | +| Channel | Channel identifier | +| Started | Wall-clock start time | +| Duration | Elapsed wall time | + +- Toggled with `f` +- Read-only: no user interaction, no j/k navigation +- Refresh driven by `AgentEvent::FleetSnapshot`; a background tokio interval task polls `list_agent_sessions` every `[fleet] refresh_interval_secs` (default 5) and sends the event +- Session lifecycle: `upsert_agent_session` on start, `update_agent_session_status` on normal or error exit; `reconcile_stale_sessions` marks stale active rows as `unknown` on startup (single atomic UPDATE, no TOCTOU race) +- CLI subcommand `zeph agents fleet` prints the same data as a formatted table + +### Config + +```toml +[fleet] +refresh_interval_secs = 5 # default; serde(default) — no migration needed +``` + +### Key Invariants + +- `reconcile_stale_sessions` runs once at startup before any session is registered — never after +- Fleet panel is read-only; the user cannot kill or restart sessions from the TUI +- `AgentEvent::FleetSnapshot` carries the full snapshot; the panel renders it directly without querying the DB again + +--- + +## Reasoning Token Tracking (#3904, #4354) + +The Metrics panel displays reasoning tokens (thinking blocks) separately from prompt and completion tokens. + +| Metric | Description | +|--------|-------------| +| `reasoning_tokens` | Cumulative count of tokens in `` blocks for the session | + +`MetricsSnapshot::reasoning_tokens` is updated after each LLM response that contains thinking-block parts. Displayed in the Metrics panel alongside prompt/completion/cached token counts. + +--- + +## Terminal Title (#4354) + +When running in TUI mode, the terminal title is set to `Zeph — ` using ANSI escape sequences. The title is updated once at TUI startup and reset to the previous title on exit. + +--- + ## Log Fallback to Platform Log Directory When TUI mode is active with no `logging.file` configured and OTLP is disabled, `tracing_init` automatically adds a file appender using `default_log_file_path()`: diff --git a/specs/012-graph-memory/spec.md b/specs/012-graph-memory/spec.md index 86988f3a6..06aef7caf 100644 --- a/specs/012-graph-memory/spec.md +++ b/specs/012-graph-memory/spec.md @@ -332,3 +332,39 @@ link_weight_decay_interval_secs = 86400 - Decay runs independently of eviction — never couple to GC cycle timing - `retrieval_count` is incremented atomically on each successful edge traversal - NEVER apply boost to edges with `retrieval_count = 0` — baseline confidence applies unchanged + +--- + +## PRISM: Query-Sensitive A* Edge Costing (#4079, #4360) + +Opt-in traversal enhancement that weights BFS graph recall by cosine similarity between the +query embedding and each candidate entity vector, in addition to the existing confidence score. + +### Cost Function + +``` +cost(edge) = (1 - edge.confidence) * (1 - cosine_sim(query_vec, entity_vec)).max(0.01) +``` + +- Lower cost = preferred traversal path (A* semantics) +- `.max(0.01)` prevents zero-cost edges from producing degenerate traversal + +### Implementation + +1. Query embedding fetched **once per `recall_graph()` call** via the configured embed provider, bounded by a 10 s timeout +2. Entity vectors batch-fetched via `qdrant_point_ids_for_entities` + `get_vectors_from_collection` +3. On timeout or embed error: **silent fallback** to confidence-only cost (`cost = 1 - confidence`) + +### Config + +```toml +[memory.graph] +query_sensitive_cost = false # default — no behavior change when disabled +``` + +### Key Invariants + +- Default is `false` — PRISM is opt-in; confidence-only cost is the default behavior +- Query embedding is fetched exactly once per recall call — NEVER per entity +- On embed failure, fallback is silent (DEBUG log only) — never surface to user +- PRISM does not change the returned entity set; only the traversal order changes diff --git a/specs/013-acp/spec.md b/specs/013-acp/spec.md index d89fd21d4..7a3a17128 100644 --- a/specs/013-acp/spec.md +++ b/specs/013-acp/spec.md @@ -125,6 +125,42 @@ are available in PR4+: - When `message_ids_enabled = true`, every `PromptResponse` and every streamed chunk must carry the originating `message_id` — partial echo (response but not chunks, or vice versa) is a bug +## Session CRUD Endpoints (#3902, #4252) + +ACP exposes REST-style endpoints for session lifecycle management alongside the existing WebSocket/SSE protocol paths. + +| Method | Path | Description | +|--------|------|-------------| +| `POST /sessions` | Create new session | Returns `{ session_id, status }` | +| `GET /sessions/{id}` | Fetch session metadata | Returns current status, `working_dir`, created_at | +| `PATCH /sessions/{id}` | Update session (partial update) | Supports `working_dir` update | +| `DELETE /sessions/{id}` | Terminate session | Graceful teardown (same as `session/close`) | + +### SessionStatus Enum + +``` +running — session is active and processing messages +idle — session is open but waiting for input +stopped — session has been gracefully terminated +error — session terminated due to an unhandled error +``` + +`SessionStatus` is `#[non_exhaustive]` — callers must handle unknown variants gracefully. + +### PATCH working_dir rules + +- The new `working_dir` must be within the `additional_directories` allowlist (same gate as session init) +- Path is canonicalized via `tokio::fs::canonicalize` — no blocking worker threads +- Paths outside the allowlist return `403 Forbidden` — never silently accepted + +### Key Invariants + +- `POST /sessions` response is synchronous — the session is ready to accept messages before the response returns +- `DELETE /sessions/{id}` follows the same flush-then-remove contract as `session/close` +- `GET /sessions/{id}` returns 404 after `DELETE` — session IDs are not reused + +--- + ## Unstable Features (feature: `acp-unstable`) - `unstable-session-list`: enumerate active sessions diff --git a/specs/021-zeph-context/spec.md b/specs/021-zeph-context/spec.md index 9c8a4d9a8..d47b01a37 100644 --- a/specs/021-zeph-context/spec.md +++ b/specs/021-zeph-context/spec.md @@ -291,7 +291,74 @@ slots unchanged. --- -## 10. Open Questions +## 10. PRISM Query-Sensitive Edge Costing (#4079, #4360) + +PRISM adds an opt-in query-sensitive A* cost function to graph recall in `zeph-memory`. +When enabled, the BFS traversal weights each candidate entity by cosine similarity to the +current query embedding rather than using confidence-only cost. + +### Cost Function + +``` +cost = (1 - confidence) * (1 - cosine_sim).max(0.01) +``` + +- `confidence` = edge confidence from MAGMA typed edge +- `cosine_sim` = cosine similarity between query vector and entity vector (0.0–1.0) +- `.max(0.01)` prevents zero-cost edges from causing BFS to skip valid nodes + +### Implementation Details + +- Query embedding is fetched **once per graph recall call** — not per entity +- Entity vectors are batch-fetched via `qdrant_point_ids_for_entities` + `get_vectors_from_collection` +- Embed call is bounded by a 10 s timeout; on timeout, falls back to confidence-only cost (no error) + +### Config + +```toml +[memory.graph] +query_sensitive_cost = false # default off — no behavior change when disabled +``` + +### Key Invariants + +- Default is `false` — existing behavior unchanged without explicit opt-in +- Fallback to confidence-only cost is silent (logged at `DEBUG` only) +- NEVER fetch query embedding per entity — exactly one embed call per graph recall invocation + +--- + +## 11. DACS Summary Injection (#4080, #4360) + +DACS implements `ContextInjectionMode::Summary` for subagent context injection. +When a parent agent spawns a subagent with `injection_mode = "summary"`, the parent +constructs a deterministic, LLM-free summary to pass as the subagent's initial context. + +### Summary Construction + +1. Last user message — up to 80 chars +2. Last 3 assistant text snippets — up to 60 chars each +3. Tool-use parts are stripped +4. Newlines collapsed to spaces +5. Result char-truncated at `floor_char_boundary` to `summary_max_chars` (default 600) +6. Empty result returns `task_prompt` unchanged + +### Config + +```toml +[agent.subagent] +summary_max_chars = 600 # default +``` + +### Key Invariants + +- Summary construction is synchronous and LLM-free — no async calls, no provider dependency +- Empty summary falls back to `task_prompt` — subagent always receives at least the task +- NEVER include raw tool outputs in the summary — strip `ToolUse` and `ToolResult` parts + +--- + +## 12. Open Questions None. diff --git a/specs/028-hooks/spec.md b/specs/028-hooks/spec.md index 4e0607a15..f9baf1abd 100644 --- a/specs/028-hooks/spec.md +++ b/specs/028-hooks/spec.md @@ -234,6 +234,55 @@ validating the new config. `HookRunner::replace_config` is an atomic swap using --- +## PostToolUse Output Replacement (#3998) + +A `post_tool_use` hook can replace the tool output seen by the LLM by returning a JSON +object with `hookSpecificOutput.updatedToolOutput`. When present and non-null, the +replacement is substituted into the tool result before it enters the LLM context. + +- Multiple chained hooks are supported; the **last non-null replacement wins** +- Each subsequent hook in the chain receives the prior replacement as its stdin +- Replacements that are null or absent leave the prior output unchanged +- `duration_ms` (u64 wall-clock milliseconds of tool execution, excluding permission + prompts and PreToolUse hook time) is included in the hook input JSON for both + `post_tool_use` and `post_tool_use_failure` events + +### Output Replacement Invariants + +- Replacement applies to the tool result only — system prompt and chat messages are never modified by hook output +- An empty `updatedToolOutput` string IS a valid replacement — the tool result becomes empty +- A null `updatedToolOutput` is treated as "no replacement" — prior output unchanged + +--- + +## Inline Hooks in config.toml (#3885, #4252) + +Hooks can be defined inline in `config.toml` under the `[hooks]` section alongside other +configuration. This is the canonical inline declaration format; plugin-provided hooks via +`SKILL.md` are merged at startup. + +```toml +[[hooks.pre_tool_use]] +type = "command" +command = "echo 'about to call $ZEPH_TOOL_NAME'" + +[[hooks.pre_tool_use]] +type = "mcp_tool" +server = "my-server" +tool = "notify" +args = { tool = "$ZEPH_TOOL_NAME" } + +[[hooks.post_tool_use]] +type = "command" +command = "echo 'tool took ${ZEPH_TOOL_DURATION_MS}ms'" +``` + +All six event types (`cwd_changed`, `file_changed`, `permission_denied`, `turn_complete`, +`pre_tool_use`, `post_tool_use`) and both action types (`command`, `mcp_tool`) are supported +inline. The `[hooks]` section is optional; absent section = empty hook lists = zero-cost. + +--- + ## Key Invariants - Hook commands execute with the blocked-command list applied — dangerous shell patterns are prevented diff --git a/specs/037-config-schema/spec.md b/specs/037-config-schema/spec.md index 38e8ff3a7..40a1b0313 100644 --- a/specs/037-config-schema/spec.md +++ b/specs/037-config-schema/spec.md @@ -220,6 +220,39 @@ Writes the migrated TOML back to the config file path. - Store secrets or API keys as plain strings in any config struct. - Use `serde_yaml` or `serde_yml` — TOML only for config files. - Add `ZEPH_VAULT_BACKEND=env` as a documented or supported pattern. +- Use `String` for fields that have a closed set of valid values — use enums with `#[serde(rename_all = "lowercase")]`. + +--- + +## Stringly-Typed Field Migration (#3832, #3833, #4295, #4305, #4355) + +A series of PRs migrated stringly-typed config fields to proper Rust enums. Invalid values +now produce a deserialization error at startup instead of a runtime `warn!` log and silent fallback. + +### Migrated Fields + +| Field | Old Type | New Type | Enum Values | +|-------|----------|----------|-------------| +| `GoalConfig.supervisor_provider` | `Option` | `Option` | any `[[llm.providers]]` name | +| `OrchestrationConfig.default_failure_strategy` | `String` | `FailureStrategy` | `abort`, `skip`, `retry` | +| `PlannedTask.failure_strategy` | `String` | `Option` | same | +| `AuditConfig.destination` | `String` | `AuditDestination` | `file`, `db`, `both` | +| `StoreRoutingConfig.fallback_route` | `String` | `FallbackRoute` | `local`, `remote` | +| `VaultBackend` | `String` | `VaultBackend` enum | `age`, `env` | + +`VaultBackend` is `#[non_exhaustive]` and emits a `warn!` on unknown backend strings +(graceful degradation to default, not a hard error) (#4074). + +### ProviderName Type + +`ProviderName` is `Arc` (migrated from `String` in #4230). This allows cheap cloning +of provider references throughout the subsystem. All `*_provider` config fields use +`Option` or `ProviderName`. + +### FleetEntry.state Migration + +`FleetEntry.state: String` → `AutonomousState` enum (#4338). Values: `Idle`, `Running`, +`Paused`, `Completed`, `Failed`. `#[non_exhaustive]` for forward compatibility. --- diff --git a/specs/044-subagent-lifecycle/spec.md b/specs/044-subagent-lifecycle/spec.md index 6b3a7f70c..969c75ebd 100644 --- a/specs/044-subagent-lifecycle/spec.md +++ b/specs/044-subagent-lifecycle/spec.md @@ -276,7 +276,62 @@ AND memory content exceeding the token budget is truncated, not omitted entirely --- -## 9. Open Questions +## 9. Orchestrator Identity Fields (#4032) + +`SpawnContext` gains orchestrator identity fields so spawned subagents can identify the +parent orchestrator in their system prompt header. + +| Field | Type | Description | +|-------|------|-------------| +| `orchestrator_name` | `Option` | Name of the parent agent or orchestrator | +| `orchestrator_role` | `Option` | Role string (e.g., `"coordinator"`, `"supervisor"`) | + +When `orchestrator_role` is `None`, the spawned subagent's orchestrator header omits the +role clause entirely (no dangling pronoun). This fixes a grammar issue where the header +read "from ()" when role was absent (#4070). + +### Key Invariants + +- Both fields are optional — absent = header omits the field without breaking formatting +- `orchestrator_name` and `orchestrator_role` are injected into the subagent's system prompt only; not stored in the transcript + +--- + +## 10. MCP Config Inheritance on Respawn (#4342) + +When a subagent is respawned (e.g., after crash or explicit restart), the parent's MCP +config is re-inherited automatically. Previously, the MCP server list was captured at +initial spawn time and lost on respawn. + +### Mechanism + +`SpawnContext::mcp_tool_names` is updated from the live parent `McpManager` reference +at respawn time, not from a stale snapshot. + +### Key Invariants + +- Respawned subagents MUST inherit the parent's current MCP config, not the config at initial spawn +- MCP tool names list is never cached in `SubAgentDef` — always fetched live from parent manager + +--- + +## 11. Worktree Teardown Safety (#4342) + +When a subagent operating in a git worktree is stopped, the teardown sequence checks +whether the worktree is still referenced by any other active subagent before removing it. + +- Teardown is blocked if another agent has the worktree as its cwd +- Teardown proceeds if no active agents reference the worktree +- `git worktree remove` is invoked only if both conditions hold: worktree is not referenced AND the path matches the `worktrees/` prefix + +### Key Invariants + +- NEVER remove a worktree that is still in use by an active subagent +- NEVER remove a path that was not created by Zeph (must match known prefix pattern) + +--- + +## 12. Open Questions None. diff --git a/specs/058-plugins/spec.md b/specs/058-plugins/spec.md index ed9be2781..c06b1af00 100644 --- a/specs/058-plugins/spec.md +++ b/specs/058-plugins/spec.md @@ -450,7 +450,74 @@ not by inode or creation order. This ensures reproducible overlay merge results --- -## 12. Open Questions +## 12. Auto-Update Policy (#3902, #4252, #4289) + +`PluginMeta::auto_update: bool` (default `false`) is an opt-in field declared in the +plugin manifest. When `true`, `PluginManager::enforce_auto_update()` checks for updates +on startup and schedules periodic refresh. + +### Behavior + +- `auto_update = false` (default): plugin is never auto-updated; manual update only +- `auto_update = true`: `PluginManager` checks for a newer version on every startup + and updates in the background via `spawn_blocking` +- Update check is non-blocking — startup proceeds regardless of update outcome + +### `add_remote()` Fix (#4318) + +The blocking `add()` call inside `add_remote()` is now dispatched via +`tokio::task::spawn_blocking` to prevent stalling the async executor on disk I/O during +remote plugin installation. + +### Key Invariants + +- `auto_update` is declared in the plugin manifest, not in agent config — plugin author controls this +- Auto-update MUST NOT overwrite a locally-modified plugin without re-verifying the integrity hash +- NEVER perform `add_remote()` synchronously on the async executor thread + +--- + +## 13. Plugin Dependency Enforcement (#4312) + +Plugins can declare `requires` dependencies in their manifest. `PluginManager` enforces +these before activation. + +### Dependency Types + +- `skills`: list of skill names that must be active +- `mcp_servers`: list of MCP server names that must be configured +- `features`: list of compile-time feature flags that must be enabled + +### Enforcement + +`PluginManager::activate()` checks all declared dependencies before injecting the plugin's +config overlay and skills. Missing dependencies result in `PluginError::DependencyNotMet` +with a human-readable message listing which dependencies failed. + +### Key Invariants + +- Dependency check runs BEFORE config overlay merge — a plugin with unsatisfied deps never modifies agent config +- NEVER activate a plugin with unresolvable skill or MCP dependencies silently + +--- + +## 14. Projected Context Cost in TUI (#4312) + +The TUI Plugin panel displays a **projected context cost** for each active plugin: +estimated tokens the plugin's skills and config will consume per turn. + +| Column | Description | +|--------|-------------| +| Name | Plugin name | +| Skills | Count of active skills from this plugin | +| Projected Cost | Estimated token cost per turn | +| Status | `active`, `inactive`, `error` | + +This helps operators identify token-heavy plugins before they cause context pressure. + +--- + +## 15. Open Questions None. diff --git a/specs/README.md b/specs/README.md index 2e4de4d40..1e792cd28 100644 --- a/specs/README.md +++ b/specs/README.md @@ -79,7 +79,7 @@ Spec IDs (001–044) follow a logical grouping: | `004-memory/004-15-memory-skill-coevolution-rfc-decision.md` | RFC #4218 decision: memory–skill coevolution analysis (MemQ, δ-mem, EvolveMem, SAGE-GraphMem, NanoResearch, Cognifold); adopt Cognifold idle-time folding + EvolveMem feedback routing; defer MemQ to P3 (#4218) | `zeph-memory`, `zeph-skills` | | `005-skills/spec.md` | SKILL.md format, registry, matching, hot-reload, skill trust governance, two-stage matching, Wilson score confidence intervals, hub install pipeline, agent-invocable skills (`invoke_skill`) | `zeph-skills` | | `006-tools/spec.md` | ToolExecutor, CompositeExecutor, TAFC, schema filter, result cache, dependency graph, tool invocation phase taxonomy, native `tool_use` only; `invoke_skill`/`load_skill` utility-gate exemption | `zeph-tools` | -| `007-channels/spec.md` | Channel trait, AnyChannel dispatch, streaming, channel feature parity, `stream_interval_ms` (Bot API 10.0, #3727); `TelegramApiClient` 30s `REQUEST_TIMEOUT` on reqwest client (#3780); Telegram reaction moderation tools `telegram_delete_reaction` / `telegram_delete_all_reactions` (#3770) | `zeph-channels` | +| `007-channels/spec.md` | Channel trait, AnyChannel dispatch, streaming, channel feature parity, `stream_interval_ms` (Bot API 10.0, #3727); `TelegramApiClient` 30s `REQUEST_TIMEOUT` on reqwest client (#3780); Telegram reaction moderation tools `telegram_delete_reaction` / `telegram_delete_all_reactions` (#3770); CJK false-positive fix in FeedbackDetector; `send_status` added to Discord and Slack adapters (#4228) | `zeph-channels` | | `007-channels/007-1-telegram-guest-mode.md` | Telegram Guest Mode — `guest_message` update handling, `answerGuestQuery` routing, `allowed_users` access control, single-shot streaming (#3729) | `zeph-channels`, `zeph-core`, `zeph-config` | | `007-channels/007-2-telegram-bot-to-bot.md` | Telegram Bot-to-Bot — `setManagedBotAccessSettings` startup, `allowed_bots` authorization, reply-chain loop prevention, `is_from_bot` metadata (#3730) | `zeph-channels`, `zeph-core`, `zeph-config` | | `008-mcp/spec.md` | MCP client, server lifecycle, semantic tool discovery, per-message pruning cache, injection detection, tool collision detection, caller identity propagation, tool quota, structured error codes, OAP authorization, elicitation (2025-06-18) | `zeph-mcp` | @@ -88,7 +88,7 @@ Spec IDs (001–044) follow a logical grouping: | `010-security/010-5-egress-logging.md` | Egress logging sub-spec: `EgressEvent` per outbound HTTP call, `AuditEntry.correlation_id`, bounded mpsc telemetry (256 + drop counter), TUI Security panel surface | `zeph-tools`, `zeph-core`, `zeph-tui` | | `010-security/010-6-vigil-intent-anchoring.md` | VIGIL verify-before-commit sub-spec: pre-sanitizer regex tripwire with Block/Sanitize action, per-turn `current_turn_intent`, subagent exemption, non-retryable blocks via `error_category="vigil_blocked"` | `zeph-core`, `zeph-tools`, `zeph-config` | | `010-security/010-7-shadow-memory-guardrail.md` | Shadow Memory Guardrail sub-spec: MAGE multi-turn threat detection (goal hijacking via accumulating risk scores), SafeHarbor hierarchical guardrail tree (entropy-based evolution, adaptive rule injection) | `zeph-sanitizer`, `zeph-memory`, `zeph-agent-tools`, `zeph-core` | -| `011-tui/spec.md` | ratatui dashboard, spinner rule for background operations, TuiChannel, RenderCache, embed backfill progress, multi-session `SessionRegistry`, `/session` commands, compact paste indicator | `zeph-tui` | +| `011-tui/spec.md` | ratatui dashboard, spinner rule for background operations, TuiChannel, RenderCache, embed backfill progress, multi-session `SessionRegistry`, `/session` commands, compact paste indicator; Fleet panel (`f` key, #3884); reasoning token tracking; terminal title (#4354); fleet session lifecycle wiring (#4363) | `zeph-tui` | | `012-graph-memory/spec.md` | Entity graph, BFS recall, community detection, MAGMA typed edges, SYNAPSE spreading activation | `zeph-memory` | | `004-memory/004-6-graph-memory.md` | Graph memory sub-spec (concise reference within 004-memory): MAGMA typed edges, SYNAPSE config, A-MEM link weights, key invariants | `zeph-memory` | | `013-acp/spec.md` | ACP transports, session management, permissions, fork/resume, session/close handlers, capability advertisement, /agent.json endpoint | `zeph-acp` | @@ -107,7 +107,7 @@ Spec IDs (001–044) follow a logical grouping: | `026-classifiers/spec.md` | Candle-backed ML classifiers: injection detection, PII detection, LlmClassifier for feedback, unified regex+NER sanitization pipeline | `zeph-classifiers` | | `027-tui-subagent-management/spec.md` | Interactive TUI subagent sidebar (a key), j/k navigation, Enter loads transcript, Esc returns, Tab cycling; automatic view switch to foreground subagent on spawn + return to Main on completion via `ForegroundSubagentStarted`/`ForegroundSubagentCompleted` `AgentEvent` variants (#3764) | `zeph-tui` | | `028-runtime-layer/spec.md` | RuntimeLayer middleware with before_chat/after_chat/before_tool/after_tool hooks, NoopLayer, LayerContext, unwind guards; plugin config overlay merge (tighten-only) | `zeph-core` | -| `029-hooks/spec.md` | Reactive hooks: cwd_changed / file_changed / permission_denied / turn_complete / pre_tool_use / post_tool_use events, set_working_directory tool, FileChangeWatcher, ZEPH_TOOL_NAME / ZEPH_TOOL_ARGS_JSON / ZEPH_SESSION_ID env vars (#3725); pre_tool_use fires before utility gate (#3738); permission_denied fires at all gate denial points in tier loop (#3779); turn_complete McpTool hooks wired to live MCP dispatch (#3776) | `zeph-core` | +| `029-hooks/spec.md` | Reactive hooks: cwd_changed / file_changed / permission_denied / turn_complete / pre_tool_use / post_tool_use events, set_working_directory tool, FileChangeWatcher, ZEPH_TOOL_NAME / ZEPH_TOOL_ARGS_JSON / ZEPH_SESSION_ID env vars (#3725); pre_tool_use fires before utility gate (#3738); permission_denied fires at all gate denial points in tier loop (#3779); turn_complete McpTool hooks wired to live MCP dispatch (#3776); post_tool_use output replacement + duration_ms (#3998); inline hooks in config.toml (#3885, #4252) | `zeph-core` | | `030-feature-flags/spec.md` | Feature flag decision rules, surviving flag inventory (22 flags), bundle definitions (desktop, ide, server, full) | `Cargo.toml`, cross-cutting | | `031-tui-slash-autocomplete/spec.md` | Inline autocomplete dropdown in TUI Insert mode, reuses filter_commands registry, Tab/Enter accepts, Esc dismisses | `zeph-tui` | | `032-database-abstraction/spec.md` | PostgreSQL backend, zeph-db crate, DatabaseDriver trait, Dialect trait, sql!() macro, migrations, CLI subcommands | `zeph-db`, cross-cutting | From 1282ecaedfb5553a3c9f425da54831015f401b51 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 15:14:53 +0200 Subject: [PATCH 2/7] chore: update internal workspace dependency versions to 0.21.2 --- Cargo.toml | 58 +++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d63a786d..bf8c85527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,35 +147,35 @@ url = "2.5.8" uuid = "1.23.1" walkdir = "2.5" wiremock = "0.6.5" -zeph-a2a = { path = "crates/zeph-a2a", version = "0.21.1" } -zeph-acp = { path = "crates/zeph-acp", version = "0.21.1" } -zeph-agent-context = { path = "crates/zeph-agent-context", version = "0.21.1" } -zeph-agent-feedback = { path = "crates/zeph-agent-feedback", version = "0.21.1" } -zeph-agent-persistence = { path = "crates/zeph-agent-persistence", version = "0.21.1" } -zeph-agent-tools = { path = "crates/zeph-agent-tools", version = "0.21.1" } -zeph-bench = { path = "crates/zeph-bench", version = "0.21.1" } -zeph-channels = { path = "crates/zeph-channels", version = "0.21.1" } -zeph-commands = { path = "crates/zeph-commands", version = "0.21.1" } -zeph-common = { path = "crates/zeph-common", version = "0.21.1" } -zeph-config = { path = "crates/zeph-config", version = "0.21.1" } -zeph-context = { path = "crates/zeph-context", version = "0.21.1" } -zeph-core = { path = "crates/zeph-core", version = "0.21.1" } -zeph-db = { path = "crates/zeph-db", default-features = false, version = "0.21.1" } -zeph-experiments = { path = "crates/zeph-experiments", version = "0.21.1" } -zeph-gateway = { path = "crates/zeph-gateway", version = "0.21.1" } -zeph-index = { path = "crates/zeph-index", version = "0.21.1" } -zeph-llm = { path = "crates/zeph-llm", version = "0.21.1" } -zeph-mcp = { path = "crates/zeph-mcp", version = "0.21.1" } -zeph-memory = { path = "crates/zeph-memory", default-features = false, version = "0.21.1" } -zeph-orchestration = { path = "crates/zeph-orchestration", version = "0.21.1" } -zeph-plugins = { path = "crates/zeph-plugins", version = "0.21.1" } -zeph-sanitizer = { path = "crates/zeph-sanitizer", version = "0.21.1" } -zeph-scheduler = { path = "crates/zeph-scheduler", version = "0.21.1" } -zeph-skills = { path = "crates/zeph-skills", version = "0.21.1" } -zeph-subagent = { path = "crates/zeph-subagent", version = "0.21.1" } -zeph-tools = { path = "crates/zeph-tools", version = "0.21.1" } -zeph-tui = { path = "crates/zeph-tui", version = "0.21.1" } -zeph-vault = { path = "crates/zeph-vault", version = "0.21.1" } +zeph-a2a = { path = "crates/zeph-a2a", version = "0.21.2" } +zeph-acp = { path = "crates/zeph-acp", version = "0.21.2" } +zeph-agent-context = { path = "crates/zeph-agent-context", version = "0.21.2" } +zeph-agent-feedback = { path = "crates/zeph-agent-feedback", version = "0.21.2" } +zeph-agent-persistence = { path = "crates/zeph-agent-persistence", version = "0.21.2" } +zeph-agent-tools = { path = "crates/zeph-agent-tools", version = "0.21.2" } +zeph-bench = { path = "crates/zeph-bench", version = "0.21.2" } +zeph-channels = { path = "crates/zeph-channels", version = "0.21.2" } +zeph-commands = { path = "crates/zeph-commands", version = "0.21.2" } +zeph-common = { path = "crates/zeph-common", version = "0.21.2" } +zeph-config = { path = "crates/zeph-config", version = "0.21.2" } +zeph-context = { path = "crates/zeph-context", version = "0.21.2" } +zeph-core = { path = "crates/zeph-core", version = "0.21.2" } +zeph-db = { path = "crates/zeph-db", default-features = false, version = "0.21.2" } +zeph-experiments = { path = "crates/zeph-experiments", version = "0.21.2" } +zeph-gateway = { path = "crates/zeph-gateway", version = "0.21.2" } +zeph-index = { path = "crates/zeph-index", version = "0.21.2" } +zeph-llm = { path = "crates/zeph-llm", version = "0.21.2" } +zeph-mcp = { path = "crates/zeph-mcp", version = "0.21.2" } +zeph-memory = { path = "crates/zeph-memory", default-features = false, version = "0.21.2" } +zeph-orchestration = { path = "crates/zeph-orchestration", version = "0.21.2" } +zeph-plugins = { path = "crates/zeph-plugins", version = "0.21.2" } +zeph-sanitizer = { path = "crates/zeph-sanitizer", version = "0.21.2" } +zeph-scheduler = { path = "crates/zeph-scheduler", version = "0.21.2" } +zeph-skills = { path = "crates/zeph-skills", version = "0.21.2" } +zeph-subagent = { path = "crates/zeph-subagent", version = "0.21.2" } +zeph-tools = { path = "crates/zeph-tools", version = "0.21.2" } +zeph-tui = { path = "crates/zeph-tui", version = "0.21.2" } +zeph-vault = { path = "crates/zeph-vault", version = "0.21.2" } zeroize = { version = "1.8.2", default-features = false } [workspace.lints.rust] From 8d7fb15732dfdba654cf9aee42eb59e2c35e0978 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 15:31:36 +0200 Subject: [PATCH 3/7] fix(tools): suppress unused variable warning for pid_opt on non-unix targets --- crates/zeph-tools/src/shell/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zeph-tools/src/shell/mod.rs b/crates/zeph-tools/src/shell/mod.rs index ce3b2dfab..bac28db14 100644 --- a/crates/zeph-tools/src/shell/mod.rs +++ b/crates/zeph-tools/src/shell/mod.rs @@ -1861,6 +1861,8 @@ impl ShellExecutor { if let Some(pid) = pid_opt { send_signal_with_escalation(*pid).await; } + #[cfg(not(unix))] + let _ = pid_opt; if let Some(ref tx) = self.tool_event_tx { let _ = tx From 8e762cca465c255017b278a6a3ef635c0340ab95 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 15:54:15 +0200 Subject: [PATCH 4/7] fix(tools): gate ToolCall import on non-windows in resolve_context tests --- crates/zeph-tools/src/shell/tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zeph-tools/src/shell/tests.rs b/crates/zeph-tools/src/shell/tests.rs index 14e04aafb..02322cae4 100644 --- a/crates/zeph-tools/src/shell/tests.rs +++ b/crates/zeph-tools/src/shell/tests.rs @@ -2881,6 +2881,7 @@ mod resolve_context { use super::*; use crate::ExecutionContext; + #[cfg(not(target_os = "windows"))] use crate::executor::ToolCall; /// Build a `ShellExecutor` whose only `allowed_path` is `dir`, so sandbox checks From bbeeb988b5795d736e834a60d3ab8589fe82b0b4 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 16:55:47 +0200 Subject: [PATCH 5/7] fix(tools): skip background_runs_snapshot test on windows (no sleep command) --- crates/zeph-tools/src/shell/tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zeph-tools/src/shell/tests.rs b/crates/zeph-tools/src/shell/tests.rs index 02322cae4..ab3048e96 100644 --- a/crates/zeph-tools/src/shell/tests.rs +++ b/crates/zeph-tools/src/shell/tests.rs @@ -2841,6 +2841,7 @@ async fn shutdown_terminates_long_running_background() { } #[tokio::test] +#[cfg(not(target_os = "windows"))] async fn background_runs_snapshot_returns_active_run() { use std::time::Duration; From df201c25fe311287c685c36564446438adc91e73 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 17:11:37 +0200 Subject: [PATCH 6/7] fix(core,acp): fix windows-incompatible paths and commands in tests --- crates/zeph-acp/src/client/tests.rs | 1 + crates/zeph-core/src/agent/acp_commands.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/zeph-acp/src/client/tests.rs b/crates/zeph-acp/src/client/tests.rs index d34d1cf94..1b781c178 100644 --- a/crates/zeph-acp/src/client/tests.rs +++ b/crates/zeph-acp/src/client/tests.rs @@ -67,6 +67,7 @@ async fn subagent_env_isolation() { } #[tokio::test] +#[cfg(not(target_os = "windows"))] async fn subagent_cwd_respected() { use tokio::io::{AsyncReadExt, BufReader}; diff --git a/crates/zeph-core/src/agent/acp_commands.rs b/crates/zeph-core/src/agent/acp_commands.rs index b312589e2..64ee74644 100644 --- a/crates/zeph-core/src/agent/acp_commands.rs +++ b/crates/zeph-core/src/agent/acp_commands.rs @@ -121,9 +121,11 @@ mod tests { #[test] fn dirs_populated() { - let cfg = cfg_with_dirs(&["/tmp"]); + let tmp = std::env::temp_dir(); + let tmp_str = tmp.to_string_lossy(); + let cfg = cfg_with_dirs(&[tmp_str.as_ref()]); let out = format_acp_dirs(&cfg); - assert!(out.contains("/tmp"), "got: {out}"); + assert!(out.contains(tmp_str.as_ref()), "got: {out}"); assert!(!out.contains("(none configured)"), "got: {out}"); } From bae42c1bc664fde309c3e00b7a240d3d01a348fa Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Mon, 18 May 2026 17:32:46 +0200 Subject: [PATCH 7/7] fix(core): use tempdir with canonical path in dirs_populated test --- crates/zeph-core/src/agent/acp_commands.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/zeph-core/src/agent/acp_commands.rs b/crates/zeph-core/src/agent/acp_commands.rs index 64ee74644..ddbab9faa 100644 --- a/crates/zeph-core/src/agent/acp_commands.rs +++ b/crates/zeph-core/src/agent/acp_commands.rs @@ -121,11 +121,16 @@ mod tests { #[test] fn dirs_populated() { - let tmp = std::env::temp_dir(); - let tmp_str = tmp.to_string_lossy(); - let cfg = cfg_with_dirs(&[tmp_str.as_ref()]); + // Use a real directory so canonicalize succeeds on all platforms. + // Compare against the canonical form to handle macOS /tmp→/private/tmp + // and Windows \\?\ extended-length prefix transparently. + let tmp_dir = tempfile::tempdir().expect("tempdir"); + let canonical = + std::fs::canonicalize(tmp_dir.path()).unwrap_or_else(|_| tmp_dir.path().to_owned()); + let canonical_str = canonical.to_string_lossy(); + let cfg = cfg_with_dirs(&[canonical_str.as_ref()]); let out = format_acp_dirs(&cfg); - assert!(out.contains(tmp_str.as_ref()), "got: {out}"); + assert!(out.contains(canonical_str.as_ref()), "got: {out}"); assert!(!out.contains("(none configured)"), "got: {out}"); }