fix(gateway): persist agent delete config before archiving owned state (#7941) - #8017
Conversation
|
@Audacity88 ready for review when you have a moment. All CI checks are passing locally (cargo fmt / clippy -D warnings / cargo test --locked), and the diff is scoped to fix #7941's root cause (agent delete config now persists before the owned-state archive). Happy to address any feedback — small, medium, or large — in this same PR if it fits, or split into a follow-up if not. No rush; just letting you know this is active and ready to go. |
9b96735 to
bf3403a
Compare
|
@Audacity88 Rebased onto current master Re-applied the delete-side persist-first reorder on top of Nillth's rename-side fix — both Local validation: Ready for review. |
40058a4 to
ebce300
Compare
zeroclaw-labs#7941) Reorder `delete_agent_cascade` to match the rename path's persist-first contract: `delete_with_cascade` + `persist_and_swap` first, then `archive workspace` + `cascade_owned_state`. Pre-fix, a persist failure left config naming the agent while its workspace had been archived and its owned stores (memory / cron / acp / session) had been purged — the inverse split-brain of zeroclaw-labs#7907 in the delete direction. Add `warnings: Vec<String>` to `MapKeyResponse` so partial side-effect failures (archive dir creation, workspace `fs::rename`, per-store purge errors) are surfaced in the response body, not just the server log. The two non-agent callers of `MapKeyResponse` (`handle_map_key` create / `handle_map_key` post-replace) get an empty `warnings` array — the field is `#[serde(skip_serializing_if = "Vec::is_empty")]` so the JSON shape they emit is unchanged. Three real-sqlite regression tests cover the contract: - persist-fail aborts cleanly (agent + workspace + cron all intact) - persist-ok purges the agent + archives the workspace - partial-failure (read-only archive root) carries `warnings` in 200 OK `test_state` becomes `pub(crate)` so `api_config` regression tests can share the AppState builder — mirrors the helper Nillth added for the zeroclaw-labs#7907 rename coverage.
ebce300 to
86958ca
Compare
singlerider
left a comment
There was a problem hiding this comment.
Reviewed at head 86958ca. Fixes #7941, the delete-direction mirror of the #7907 rename split-brain. I cross-checked the reorder against the live handler and the regression tests. No prior reviews, no other blocks.
🟢 What looks good — the persist-before-side-effects reorder is correct and complete
delete_with_cascade + mark_dirty over cascade.dirty_paths() + persist_and_swap now run first; archive and cascade_owned_state run afterward from the read-back committed config. A persist failure therefore returns an error with the agent still named in config and its workspace and owned stores untouched, which is exactly the inverse split-brain #7941 describes. agent_delete_leaves_owned_state_intact_when_persist_fails proves it: persist is forced to fail (config_path is a directory), and the seeded cron row, the workspace dir, the absence of an archive dir, and the un-swapped in-memory config are all asserted.
🟢 What looks good — the slice-4 workspace-resolution lesson is preserved
workspace is still resolved via agent_workspace_dir(alias) BEFORE the cascade removes the entry, with the explanatory comment about custom-workspace fallthrough. Only data_dir is read from the post-swap config, which is safe because data_dir is global, not per-agent. That is the right split: per-agent state captured pre-mutation, global state read post-commit.
🟢 What looks good — partial failures are surfaced, not just logged
MapKeyResponse.warnings: Option<Vec<String>> with skip_serializing_if = Option::is_none keeps the generic create/delete map-key responses byte-identical (they set None) while the agent-delete path now combines archive-dir, workspace-rename, and cascade_owned_state per-store failures into the 200 body. agent_delete_response_carries_partial_failure_warnings exercises the read-only-archive-root path end to end. The test_state re-export mirrors the helper from the rename coverage rather than widening any production surface.
Verdict: APPROVED.
Audacity88
left a comment
There was a problem hiding this comment.
Reviewed current head 86958ca against live PR state, #7941, the related #7940 rename-side fix, the gateway/config source, and the visible CI results. I did not run local Cargo validation in this review-only pass. I agree with @singlerider's approval: the delete-side fix matches the accepted persist-first contract.
🟢 What looks good — persist happens before destructive delete side effects
delete_agent_cascade now resolves the agent workspace while the agent entry still exists, runs delete_with_cascade, marks every dirty cascade path, and calls persist_and_swap before archiving the workspace or purging owned stores. That is the important #7941 safety property. If persistence fails, the handler returns before moving the workspace or touching cron/memory/acp/session state, so config and owned state stay consistent.
🟢 What looks good — post-persist partial failures now reach the caller
The new optional MapKeyResponse.warnings field is only populated on the agent-delete path. Generic map-key create/delete responses keep the old JSON shape when there are no warnings. The delete path combines archive-dir, workspace-rename, and cascade_owned_state warnings into the response body, so a committed config delete with a failed side effect is no longer a clean-looking 200 that only leaves evidence in server logs.
🟢 What looks good — regression coverage hits the right boundary
The tests cover the three load-bearing cases: persist failure leaves the cron row and workspace intact, successful persist removes the config entry and then purges/archives owned state, and a post-persist archive-side failure returns a warnings array. I also agree with @singlerider that preserving the pre-delete workspace resolution is the right carry-over from the rename-side fix, especially for custom workspace paths.
One merge-readiness note, separate from this approval: current master has moved over api.rs, api_config.rs, and adjacent config/cron surfaces since this branch's base. GitHub reports the branch as cleanly mergeable, but this high-risk gateway path should still get a branch refresh and refreshed CI, or equivalent merge-result evidence, before landing.
|
Thanks @Audacity88 for the merge, and thanks to @singlerider and @Audacity88 for the reviews on the delete-side persist-first fix and the partial-failure warnings. 🙏 |
#38) * test(zerocode): cover insecure-TLS confirmation flow (#7693) (#8178) - ff4cece test(zerocode): cover insecure-TLS confirmation flow (#7693) - 95edace Merge branch 'master' into fix/issue-7693-zerocode-insecure-tls-confi… * fix(runtime): strip /think directive before per-turn tool filter on process_message (#8054) (#8216) - 3a350d4 fix(runtime): strip /think directive before per-turn tool filter on p… - e9e15a3 Merge branch 'master' into fix/issue-8054-surface4-think-directive-to… * test(gateway): cover auth rate limiter loopback detection and lockout window (#8271) - 38abc41 test(gateway): cover auth rate limiter loopback detection and lockout… - 8cc720f Merge branch 'master' into alix/test-gateway-rate-limit * test(runtime): pin approval-required and control branches of should_execute_tools_in_parallel (#7686) (#8222) - 2fa7be2 test(runtime): pin approval-required and control branches of should_e… - 9d47fb0 Merge branch 'master' into fix/issue-7686-approval-tool-batch-ordering * test(memory): cover future timestamp decay clamp (#8297) - bfe95e0 test(memory): cover future timestamp decay clamp - 25359fb Merge branch 'master' into test-memory-decay-future-timestamp * test(memory): cover namespace quota violation payload (#8298) - 0e99ebf test(memory): cover namespace quota violation payload - 46896bf Merge branch 'master' into test-memory-namespace-quota * docs(providers): fill configuration examples (#7706) - 5132b2b docs(providers): fill configuration examples - 542d316 Merge branch 'master' into alix/x11-provider-config-docs * docs(maintainers): align checked-in label references (#8240) - 1a67b65 docs(maintainers): align checked-in label references - 7fa4211 Merge branch 'master' into codex/issue-6808-canonical-label-references * fix(runtime): cap shell subprocess memory (#7937) - 17e1956 fix(runtime): cap shell subprocess memory - 70865fe fix(runtime): compile memory limits across platforms - 0b39b81 fix(runtime): handle windows process handles - d73b3fd fix(runtime): split unix rlimit conversion - 6d91976 fix(runtime): classify language memory errors - 5d464d1 Merge branch 'master' into codex/issue-6916-shell-memory-limit - 8ea10d9 Merge branch 'master' into codex/issue-6916-shell-memory-limit - 274975f docs(skills): clarify shell memory limit scope - 2a7395e Merge branch 'master' into codex/issue-6916-shell-memory-limit * fix(gateway): persist agent delete config before archiving owned state (#7941) (#8017) - 86958ca fix(gateway): persist agent delete config before archiving owned state (#7941) - 163aa28 Merge branch 'master' into fix/issue-7941-delete-agent-persist-order - 932d8d5 fix(gateway): include warnings in cascade delete response - 8b3e591 Merge branch 'master' into fix/issue-7941-delete-agent-persist-order * test(providers): pin responses-wire option propagation (#7690) (#8037) - b85ea2b test(providers): pin responses-wire option propagation (#7690) - c18a643 test(providers): document supported vs unsupported responses-wire options (#7690) - 0aeeca3 ci: re-trigger Quality Gate on #8037 - fee4df0 docs(providers): enumerate responses-path runtime options that are not yet wired - fa2a7e9 feat(providers): wire provider_timeout_secs and extra_headers through responses path - 847a96a Revert "feat(providers): wire provider_timeout_secs and extra_headers through responses path" - 0a958fb Merge branch 'master' into fix/issue-7690-responses-wire-options * ci(workflows): add lockfile integrity check and npm dependency review (#8155) - 8880007 ci(workflows): add lockfile integrity check and npm dependency review - 2b20d2d fix: correct dependency-review-action SHA pin - e62fbfb Merge branch 'master' into ci/lockfile-and-npm-review * fix(config): surface model_routes and embedding_routes via Configurable and #[nested] (#7855) - eeb60e5 fix(config): surface model_routes and embedding_routes via Configurable and #[nested] - a213623 ci: re-trigger CI to verify flaky test - a5de2c1 fix(config): add #[group] to route fields and map_key_sections tests - 703189c fix(config): remove dead credential_class on nested Vec fields, add schemars x-secret on route api_key - 5701956 fix(config): add curated sections and serde(default) rationale for route configs - 4124322 Merge branch 'master' into fix/7701-model-route-configurable - 38b01bc Merge branch 'master' into fix/7701-model-route-configurable - 69abec7 Merge branch 'master' into fix/7701-model-route-configurable * fix(runtime): strip orphaned tool_use on max-iterations exit (#7865) - 90aaf44 fix(runtime): strip orphaned tool_use on max-iterations exit - 87f6afa fix(runtime): drop content-null orphan dispatch instead of corrupting it - b98fd3f Merge branch 'master' into fix/strip-orphaned-tool-use-max-iter - 3fe6f5b Merge branch 'master' into fix/strip-orphaned-tool-use-max-iter * docs(zerocode): document session picker workflow (#7860) - 3e4215a docs(zerocode): document session picker workflow - 5725d97 docs(zerocode): address session-picker review feedback - 74f5541 Merge branch 'master' into fix/zerocode-session-docs-7746 * fix(providers): distinguish missing vs expired OpenAI Codex credentials (#8029) The call-time credential resolver returned the same "auth profile not found" error whether no Codex profile existed at all, or a profile existed but its access token was expired and could not be refreshed. The latter case misattributed a re-authentication problem to a missing profile, sending users into a login loop pointed at the wrong fix. Branch the error on whether a profile was actually loaded: - no profile -> "No OpenAI Codex credentials found. Run auth login ..." - stale token -> "credentials are present but expired or could not be refreshed. Re-run auth login ..." Message-only change; the auth/resolution control flow is unchanged. * fix(telegram): bypass mention_only gate for replies to bot messages (#7958) - 8c15897 fix(telegram): bypass mention_only gate for replies to bot messages - bb8d0a2 fix(telegram): add tests for reply-to-bot mention_only gate bypass - f7dd595 fix(telegram): fix reply-to-bot tests and clippy::collapsible_if - 54a51ad chore: cargo fmt * test(tools): cover hardware_board_info execute paths (#7867) - 9ceb36b test(tools): cover hardware_board_info execute paths - ba808d3 test(tools): add memory-read and probe-fallback execute coverage * test(config): cover schema_markdown string helpers (#8243) - 5fdf8e2 test(config): cover schema_markdown string helpers * test(memory): cover qdrant backend classification and profile keys (#8268) - 5c62552 test(memory): cover qdrant backend classification and profile keys * fix(test): make control-plane PID liveness tests deterministic (#8242) - d6f6fb5 fix(test): make control-plane PID liveness tests deterministic * fix(runtime): warn when systemd user lingering is disabled (#8249) - 8c467b1 fix(runtime): warn when systemd user lingering is disabled * test(discord): cover custom id kind escaping (#8300) - 23145f1 test(discord): cover custom id kind escaping - ee08d15 Merge branch 'master' into test-discord-custom-id-escape * test(channels): cover allowlist wildcard matcher short-circuit (#8299) - c75119f test(channels): cover allowlist wildcard matcher short-circuit - 0e52c22 Merge branch 'master' into test-channels-allowlist-wildcard * ci(workflows): add CycloneDX SBOM generation for Rust and npm (#8158) - bb835f0 ci(workflows): add CycloneDX SBOM generation - e13f1b4 Merge branch 'master' into ci/sbom-generation * fix(observability): CLI one-shot loses telemetry and token totals on exit (#8146) - ff9fc19 fix(observability): flush telemetry before CLI one-shot process exit - 30395de fix(observability): populate tokens_used in CLI path AgentEnd event - 17a6ea9 Merge branch 'master' into fix/flush-guard-cli-telemetry - d9ad3a6 Merge branch 'master' into fix/flush-guard-cli-telemetry - 71459ab Merge branch 'master' into fix/flush-guard-cli-telemetry - 525db3f Merge branch 'master' into fix/flush-guard-cli-telemetry - 2570d30 Merge branch 'master' into fix/flush-guard-cli-telemetry - 708c2f7 Merge branch 'master' into fix/flush-guard-cli-telemetry * fix(zerocode): render only the viewport in long sessions (#8330) - 6de2def fix(zerocode): render only the viewport in long sessions - 26c0857 fix(zerocode): bound copy-region rebuild to the viewport * feat(sop): out-of-band approval plane with fail-closed timeout and PriorityBased gate fix (#8304) - feat(sop): approval-plane types - principal, decision, ledger (EPIC C, C0) - feat(sop): approval config - ApprovalMode + ApprovalTimeoutAction (EPIC C, C1) - feat(sop): resolve_gate chokepoint + clear_waiting_gate extraction (EPIC C, C3) - fix(sop): fail-closed approval timeout, drop Critical/High auto-approve (EPIC C, C2) [SEC-FLIP] - fix(sop): gate PriorityBased Critical/High runs (EPIC C, C7) [SEC-FLIP] - feat(sop): route sop_approve through resolve_gate, honor approval_mode (EPIC C, C4) - docs(sop): append-only store ledger is the approval audit of record (EPIC C, C5) - feat(sop): gateway out-of-band approval routes /admin/sop/* (EPIC C, C6) - feat(sop): CLI sop approve/deny/pending via the running daemon (EPIC C, C8) - feat(sop): WS kind:sop approval frames resolve gates (EPIC C, C9) - fix(sop): route new SOP CLI verb output through fluent i18n (EPIC C, C8) - fix(sop): gate gateway SOP CLI verbs behind agent-runtime; WS let-else + deny reason (EPIC C review) - refactor(sop): retire legacy Memory approval audit; rebuild metrics from gate ledger (EPIC C) - refactor(sop): meter approval completion at the resolve_gate chokepoint - i18n(sop): localize gateway WebSocket SOP approval error frames (EPIC C) - fix(sop): fail closed when the approval-gate audit row cannot be persisted - fix(sop): restore deterministic checkpoint resume + harden gate transitions (review) * feat(acp): add opt-in MCP support for standalone ACP sessions (#8237) - 014eeda feat(acp): add opt-in MCP support for standalone ACP sessions - d243b5e refactor(acp): make MCP opt-in a per-agent setting - 877a967 fix(acp): restore ACP sessions under their owning agent - 6ee4a30 fix(acp): release sessions lock before agent/MCP startup in session/new - 93e0741 ci: re-trigger checks (prior Test job hit a runner disk-space error) - d8b2f71 Merge branch 'master' into feat/acp-enable-mcp * fix(ci): defer stable-pointer tag check to deploy time (#8344) - 2441512 fix(ci): defer stable-pointer tag check to deploy time - 91314ad fix(ci): defer stable-pointer publish until version dir exists - 7bd7959 Merge branch 'master' into ci/docs-stable-pointer-defer-tag-check * test(tools): cover email_imap TLS close_notify detection (#8346) - 0e253b6 test(tools): cover email_imap TLS close_notify detection - 6cad799 Merge branch 'master' into test/email-imap-tls-close-notify * test(tools): cover report template substitution safety and html escaping (#8270) - 014e15a test(tools): cover report template substitution safety and html escaping - 5cfe2e0 Merge branch 'master' into alix/test-report-templates * docs(labels): document ACP channel label (#8406) * fix(agent/loop-detector): do not count failed tool results as "no progress" (#8213) - cf486de fix(agent/loop-detector): do not count failed tool results as "no progress" - bfa6557 fix(agent/loop-detector): gate the hash-based identical-output abort on success too - a077711 Merge branch 'master' into fix/loop-detector-skip-failed-results * fix(runtime): forward narration emitted after a native tool call (#8329) - b2d3b26 fix(runtime): forward narration emitted after a native tool call - 3e34e60 Merge branch 'master' into fix/zerocode-narration-flush * fix(provider): cool down rate-limited fallback entries (#8317) - aa877fa fix(provider): cool down rate-limited fallback entries - 0053fc2 Merge branch 'master' into codex/issue-6074-provider-rate-limit-cooldown Co-authored-by: Aleksandr Prilipko <31770101+zverozabr@users.noreply.github.com> Co-authored-by: SimianAstronaut7 <79373020+SimianAstronaut7@users.noreply.github.com> * docs(mdbook): escape generated CLI placeholders (#8204) - 8ca08eb docs(mdbook): escape generated CLI placeholders - 831f70c docs(mdbook): use table for html tag escape check - 4497d8e Merge branch 'master' into codex/issue-7269-cli-placeholder-escaping * test(tools): cover pushover notification shape overlap (#8356) - 382b4db test(tools): cover pushover notification shape overlap - 42b0ca8 Merge branch 'master' into codex/issue-6165-pushover-notify-proof * fix(cli): add confirmation feedback after secret prompt input (#7856) - 04444bd fix(cli): add confirmation feedback after secret prompt input - ef577ca style: apply cargo fmt to secret prompt feedback lines - 6bb5bc6 style: add i18n-exempt comments to secret prompt confirmation echoes - 0a351b9 fix(cli): replace bare eprintln! strings with Fluent i18n keys for secret prompt feedback - cc42c56 fix(cli): implement pre-submit feedback for secret prompts using crossterm raw mode - 1068feb fix(cli): harden secret_prompt with RAII raw-mode guard and consistent confirmation guards - e10b49d fix(cli): return raw buffer from secret_prompt, re-prompt on empty required input * test(eval): cover trace case parsing and suite loading (#8252) The trace fixture format and suite loader had no unit tests. Add coverage: - `TraceResponse` serde tag dispatch (text / tool_calls) with the `#[serde(default)]` token fields defaulting to 0; - `LlmTrace` defaulting `expects` when the key is omitted; - `LlmTrace::from_file` parsing a fixture from disk; - `load_suite` filtering to `*.json` and returning entries sorted by path. Test-only; no behavior change. * fix(self_test): correct comment numbering in run_full function (#8212) The comment numbering in run_full was incorrect: - Line 69: changed from '// 9.' to '// 10.' for gateway health check - Line 72: changed from '// 10.' to '// 11.' for memory round-trip check - Line 75: changed from '// 11.' to '// 12.' for WebSocket handshake check This matches the actual sequence of checks since run_quick already has 9 checks. * docs(tools): document relationship memory workflows (#8263) * fix(windows): share cmd shell command construction (#8247) - 84989b2 fix(windows): share cmd shell command construction - 8b9d112 fix(windows): name cmd shell protocol tokens * ci(release): build release artifacts from the canonical feature registry (#8343) - dadb4fe ci(release): build release artifacts from the canonical feature registry - 651a012 fix(install): align prebuilt messaging with the dist release feature set - 3a7f926 fix(install): make --preset full deliver the advertised feature set * fix(channels): localize core runtime command replies (#7858) - 5f1d109 fix(channels): localize core runtime command replies - 97b3e5c fix(channels): pass timeout text as &str to finalize_draft - 1691b39 chore: merge master and resolve en/cli.ftl conflict - ea77180 chore: merge master and resolve i18n conflicts - 7592840 chore: merge master and resolve zh-CN/cli.ftl conflict * revert(runtime): remove shell subprocess memory cap (#8417) This reverts the shell/skill subprocess RLIMIT_AS memory ceiling introduced in #7937. The cap defaulted to 512 MiB and set both rlim_cur and rlim_max, so it clamped the virtual address space of every shell and skill subprocess and could not be raised by the child. In practice this made zerocode unusable: cargo, rustc, and git fail under a 512 MiB RLIMIT_AS because their threads reserve far more virtual address space than that despite using little real memory. * fix(channels/discord): authorize autocomplete in parent-allowlisted threads (#8110) The type-4 (APPLICATION_COMMAND_AUTOCOMPLETE) arm gated the interaction with `interaction_gate(..., thread_parent = None)`, while the normal message paths resolve the thread parent and pass it to `channel_passes_filter`. In a thread whose parent channel is in `channel_ids` (but the thread id is not), normal messages pass the filter, yet autocomplete failed closed to an empty choice set — inconsistent authorization for the same user in the same thread. Resolve the thread parent from the existing `thread_channels` cache (no REST) in the autocomplete arm and pass it to the gate. A parent populated by an earlier message in the thread now authorizes autocomplete exactly as the message path does (#6829). The lookup is cache-only, so the per-keystroke path stays side-effect-free; an uncached thread still yields no completions (fail-closed) rather than issuing an unbounded REST probe on every keystroke. Closes #8103. * fix(daemon): fail fast when the gateway address is already in use (#8115) - 28b4743 fix(daemon): fail fast when the gateway address is already in use - 29af278 fix(daemon): localize gateway-bind messages and defer non-AddrInUse b… - 8a3961d Merge branch 'master' into fix/7895-daemon-gateway-reuse * feat(skills): dashboard write-guard, skipped-audit, and shadowed_by (#7963 follow-ups) (#8082) - bbeae82 feat(skills): write-guard + skipped-audit + shadowed_by for the dashb… - aee06c5 feat(web/skills): surface skipped-audit + shadowed_by on the skills page - 541013f Merge remote-tracking branch 'upstream/master' into feat/dashboard-sk… - 23541b1 Merge remote-tracking branch 'upstream/master' into feat/dashboard-sk… - 7fe83f8 refactor(skills): address review nits on dashboard follow-ups - 8120f28 Merge remote-tracking branch 'upstream/master' into feat/dashboard-sk… - 018bff7 Merge remote-tracking branch 'upstream/master' into feat/dashboard-sk… - f9af90c Merge remote-tracking branch 'upstream/master' into feat/dashboard-sk… - 0057d98 refactor(skills): parameterize cached_load over its store to isolate … - 475f2a6 Merge branch 'master' into feat/dashboard-skills-followups * feat(observability): add rotating log-persistence mode with size/date/retention rotation (#8307) - 1a73cfe feat(observability): add rotating log-persistence mode with size/date… - 094a4cc Merge branch 'master' into feat/7878-log-rotation - 48476d5 Merge branch 'master' into feat/7878-log-rotation - be37095 Merge branch 'master' into feat/7878-log-rotation * test(tools): cover attribution registry contract for unit-struct tools (#8351) - 69dd692 test(tools): cover attribution registry contract for unit-struct tools - 1428d67 Merge branch 'master' into test/attribution-registry-coverage * feat(tool:browser): allow http:// URLs in browser_open (was https-only) (#8136) - 56268dd feat(tool:browser): allow http:// URLs in browser_open - fabd6cc docs(browser): make browser_open http default posture explicit - 938ed3e docs(browser): clarify allowlist does not enforce HTTPS - 43a4267 Merge branch 'master' into worktree-allow-http * fix(channels): transcode Telegram TTS to Opus and wire typed STT provider (#7019) - 5817b97 fix(channels): send non-opus TTS via sendAudio and wire agent transcr… - b88de0c fix(channels): resolve typed transcription provider aliases in media … - a902e2f fix(channels/telegram): wire typed STT providers into channel-interna… - 24627fd fix(channels/telegram): use sendVoice after synthesize_opus guarantee… - 431f2fb fix(channels): suppress TTS on error messages - b8bca9b fix(channels): remove duplicate output_format impls and add missing m… * fix(tools/cron): accept job name in cron_update and cron_remove (#8007) Both tools previously required a UUID job_id, forcing a cron_list lookup before every update or remove. Gemini 2.5 Flash would call cron_list, get the JSON back, and echo it as a response instead of chaining to the update call. Adds resolve_job_id_or_name() to the cron store: tries exact ID match first, falls back to case-insensitive name search, errors on ambiguity. cron_update and cron_remove now resolve through this before acting, so "reschedule morning_briefing to 8am" is a single tool call. * fix(scoop): register zerocode.exe in manifest (#8276) The Scoop manifest only registered zeroclaw.exe as a shim, and zerocode.exe was missing from PATH although present in ~\scoop\apps\zeroclaw\current\zerocode.exe. Update dist/scoop/zeroclaw.json bin field from a single string to an array including both zeroclaw.exe and zerocode.exe. Fixes #8275 * test(config): cover in-root path parent normalization (#8292) * feat(ci): add arm64 docker target (#5187) - 0382abb feat(docker): cross-compile arm64 images from the source Dockerfile - 5ec9a1f ci(docker): publish arm64 for Dockerfile-based image tags - 26b06f6 Merge branch 'master' into feat/add-arm64-docker-target * fix(channels/wechat): make wechat channel can handle streaming chat response. (#7437) - 98763f3 fix(channels/wechat): make wechat channel can handle streaming chat response. * fix(runtime): read heartbeat tasks from agent workspace (#8402) Closes #8366 * test(config): cover domain input normalization (#8293) * fix(channels): serialize per-sender session persistence to prevent race (#7847) - cf50c17 fix(channels): serialize per-sender session persistence to prevent race (#7753) - 033a435 fix(channels): bind Arc before lock to fix E0716 temporary drop - ce3b8b6 fix(channels): add barrier-gated concurrency test for per-sender persist lock serialization - 2b15379 fix(channels): make concurrency test observe ordering, not just count - aaab32d fix(channels): apply cargo fmt - 5778fae fix(channels): add missing thinking_overrides field in persist-lock concurrency test - a024a46 fix(channels): refresh persist lock test contexts * fix(channels): keep channel system prompt byte-stable for prefix caching (#6360) (#8174) Closes #6360. The channel path rebuilt the system prompt every turn with volatile data (datetime, reply_target, sender, message_id, cron_add delivery hint), invalidating provider-side prompt caching on every message. This moves volatile per-turn context into a `[turn-context]` preamble on the current outgoing user turn, while keeping cached channel history clean. Memory recall output now rides in the same outgoing user turn preamble instead of being appended to the system prompt, matching the CLI shape. The runtime preamble is also always prepended when `reply_target` is non-empty, so user-controlled `[turn-context]` text cannot suppress authoritative channel context. Related: PR #6630. * feat(mcp): resource & prompt client surface with policy-gated dispatch tools (#8403) - 348c01b feat(mcp): add resource protocol types - e050d49 feat(mcp): add prompt protocol types - 81926aa feat(mcp): probe and store server capabilities at handshake - 44fd732 feat(mcp): add capability-gated resource/prompt dispatch on McpServer - 0d57931 feat(mcp): registry resource/prompt surface + mcp_resources/mcp_prompts tools (policy-gated) - 31be3c3 test(mcp): lock subagent narrowing of mcp capability tools - 4fa86a7 fix(mcp): prefix prompt list names, thread list cursor, surface isError envelopes * fix(channels): initialize persist locks in cache-stability tests (#8434) * chore(channel-matrix): upgrade Matrix Rust SDK to 0.18.0 (#8421) Bump the pinned matrix-sdk dependency in zeroclaw-channels from 0.17 to 0.18.0 and re-resolve Cargo.lock. This pulls Ruma 0.16.0 (ruma-client-api 0.24, ruma-common 0.19, ruma-events 0.34, ruma-html 0.8, ruma-macros 0.19) and the matching matrix-sdk-base/common/crypto/sqlite/store-encryption 0.18 crates. The 0.18 breaking changes (RumaApiError becoming a UiaaResponse alias and Pusher::set gaining an append parameter) are not on any path the Matrix channel uses, so no source changes are required. This lands the upstream fix matrix-rust-sdk#6594 for the Arc<ClientInner> reference cycle reported in matrix-rust-sdk#6573, which caused the per-reload memory leak tracked in #6651. * docs(agents.md): sync paths and crate list with current workspace layout (#8341) - 349a88f docs(agents.md): sync paths and crate list with current workspace layout - 88a2d1c Merge branch 'master' into docs/agents-md-sync-paths * feat(approval): route tool approvals to a distinct approver channel (#8231) - a75cbae feat(approval): route tool approvals to a distinct approver channel - 39f034d Merge remote-tracking branch 'upstream/master' into feat/a2a-approval… - 7d7e4db Merge remote-tracking branch 'upstream/master' into feat/a2a-approval… - 9c26bf5 feat(approval): honor approval_route on the non-interactive turn path - d17cfb8 docs(autonomy): clarify approver_channel is a platform-qualified regi… - bc64539 Merge branch 'master' into feat/a2a-approval-router - 133144b Merge branch 'master' into feat/a2a-approval-router * ci(release): fail early on package token access (#8352) - 6e10dda ci(release): fail early on package token access - 5ae71a9 Merge branch 'master' into codex/release-publish-preflight - f972756 Merge branch 'master' into codex/release-publish-preflight * perf(web-search): cache strip_tags regex in a LazyLock static (#8350) - cbb0bfd perf(web-search): cache strip_tags regex in a LazyLock static - 33dbdfe Merge branch 'master' into fix/strip-tags-lazy-regex * fix(acp-bridge): strip UTF-8 BOM from config.toml before TOML parsing (#8326) - 821d95a fix(acp-bridge): strip UTF-8 BOM from config.toml before TOML parsing - cff90f7 Merge branch 'master' into fix/acp-bridge-bom-tolerance * docs(runtime): document max_history_messages hard cap alongside whole-turn trim (#8436) - 66584e7 docs(runtime): document history-msg hard cap alongside whole-turn trim - 997780c Merge branch 'master' into fix/8369-history-trim-docs-contract * refactor(log): extract JSONL write pipeline into testable helper (#8437) - 0f214e2 refactor(log): extract JSONL write pipeline into testable helper - 7981583 Merge branch 'master' into refactor/log-jsonl-write-helper * feat(obs): capture prompt/completion content on llm.response spans (#6966) - 6df195a feat(obs): add LlmMessageSnapshot scaffolding to LlmResponse event - 209e932 feat(obs): add credential-scrubbing capture_llm_messages helper - 110c476 feat(obs): capture llm.call messages at both LlmResponse emission sites - d466595 feat(obs): emit gen_ai.input/output.messages + system_instructions on… - e6bd51c docs(obs): note gen_ai message attrs + trace cost/privacy profile for… - faf1a03 style(obs): apply rustfmt to new capture/serializer tests - 73288d4 Merge remote-tracking branch 'upstream/master' into feat/otel-genai-l… - 3725b9e docs(obs): replace prose em-dash to satisfy docs-style gate - 0c2358d Merge remote-tracking branch 'upstream/master' into feat/otel-genai-l… - 8982324 Merge branch 'master' into feat/otel-genai-llm-messages - 4ca2c87 Merge branch 'master' into feat/otel-genai-llm-messages - 52e7698 Merge branch 'master' into feat/otel-genai-llm-messages * feat(config): add independent delegate targets (#8239) - 98f1b04 feat(config): add independent delegate targets - 35d3739 fix(runtime): block always_ask independent delegates - c246ece fix(runtime): allow agentic delegates without tools - 092bc45 test(runtime): migrate delegate rename cascade test - 88a0554 fix(runtime): avoid background delegate reauthorization - cb27b82 fix(runtime): resolve delegate mode through roster - ee761ce Merge branch 'master' into feat/independent-delegates - 51c9a89 docs(delegate): explain delegation invariants - f9d79ea Merge branch 'master' into feat/independent-delegates * ci(release): cosign signing, SLSA provenance, and SBOM for release pipeline (#8058) (#8404) - a79e87d ci: add cosign signing, SBOM generation, and SLSA provenance to relea… - ddd0b8a fix(ci): move continue-on-error from job level to SLSA generator input - 330c905 fix(ci): gate SLSA upload behind publish, scope OIDC to signing jobs,… - 46d2d46 ci(release): add cosign signing to stable release docker job - 633c125 Merge branch 'master' into ci/release-signing-sbom-provenance * test(mcp): regression-test mcp_bundles enforcement + warn on no-bundle misconfig (#8370) - 95f2aa9 test(runtime): cover unscoped-agent zero-MCP path in deferred mode (#7733) - 771ab9b test(runtime): cover unscoped-agent zero-MCP path in eager mode (#7733) - c218509 fix(config): warn when mcp.servers configured but no mcp_bundles exist (#7733) - 53373e6 test(channels): pin resolver contract for orchestrator MCP scoping (#7733) - 7410e11 test(gateway): cover append_scoped_mcp_tools no-bundle no-op (#7733) - 90b839d docs(mcp): note bundle-restart semantics + link agent.rs to regression tests (#7733) * fix(cost): atomic ledger appends + recover concatenated records (#8412) - f4420a9 fix(cost): atomic ledger appends + recover concatenated records - 37ff966 fix(cost): route malformed-record warning through typed attrs * feat(elicitation): ACP multiple-choice for the ACP channel and Zerocode Code tab (#8338) - 2e4128e feat(api): add ElicitationCapabilities parser for ACP elicitation RFD - 4568159 feat(api): add ElicitationRequest/Response/Mode wire types - 3740b61 feat(channels): plumb client elicitation capabilities to AcpChannel - 42a594c feat(channels): add single_select_schema helper + sensitive-name trip… - 2c04577 feat(channels): route ACP request_choice through elicitation/create w… - bf23402 feat(channels): add request_multi_choice + ACP elicitation multi-sele… - 21e94d4 feat(tools): poll uses ACP elicitation when channel advertises form s… - fe99b7a docs: repoint elicitation RFD comments at Phase 1 spec - 1a84c24 docs(changelog): note ACP elicitation Phase 1 (multiple-choice) - 0acaa0c Merge from origin/master - 29ad66f feat(zerocode): wire ACP elicitation into the Code tab (Phase 2 modal) - a4fa4d1 fix(zerocode): elicitation modal claims focus; drop gitignored spec refs - 71689cb Merge branch 'master' into feat/acp-elicitation - 9f6453b docs(zerocode,api): drop reviewer handles from comments; refresh stal… * docs(labels): document retained scoped labels (#8418) * fix(providers): clean Anthropic tool schemas before native serialization (#7961) - 86895ad fix(providers): clean Anthropic tool schemas before native serialization - b7a4878 chore: cargo fmt - ca237e1 Merge branch 'master' into fix/anthropic-schema-cleaning * fix(providers): omit tool_choice when the tool list is empty (#7864) - cf73d01 fix(providers): omit tool_choice when the tool list is empty - 19c1356 docs(providers): correct tool_choice comment; harden regression test - fa7be20 Merge branch 'master' into fix/openai-compat-tool-choice-empty - c061b89 Merge branch 'master' into fix/openai-compat-tool-choice-empty * fix(provider): prefer chatgpt_account_id claim in Codex JWT account extraction (#8002) - 753d700 fix(provider): parse https://api.openai.com/auth as nested object for chatgpt_account_id - ad1ab1f fix(provider): collapse nested if-let to satisfy clippy::collapsible_if - 95c705a chore: cargo fmt - 6c63801 Merge branch 'master' into fix/codex-chatgpt-account-id-claim - 3a3f94b Merge branch 'master' into fix/codex-chatgpt-account-id-claim * fix(providers): promote tool-result image markers to image_url on native tool calls (#8339) - 39dad52 fix(providers): promote tool-result image markers to image_url on native tool calls - 3611d2d Merge branch 'master' into fix/8327-tool-image-parts - 25ba308 Merge branch 'master' into fix/8327-tool-image-parts * fix(docs): silence mdbook placeholder warnings (#8435) * docs(architecture): document runtime state ownership (#8422) - f2a8640 docs(architecture): document runtime state ownership - 67590c6 Merge branch 'master' into codex/issue-6808-runtime-state-persistence * ci(workflows): add monthly cargo outdated dependency scan (#8176) - 0958d56 ci(workflows): add monthly cargo outdated dependency scan - 5e79862 fix(ci): propagate outdated scan crashes - 094c0b8 Merge branch 'master' into ci/cargo-outdated * fix(runtime): skip NO_REPLY sentinel in cron and heartbeat delivery (#8405) - 6c9b0fc fix(runtime): skip NO_REPLY sentinel in cron and heartbeat delivery - ab9b379 fix(runtime): narrow NO_REPLY suppression to quiet forms; cover deliv… - 15b95f1 Merge remote-tracking branch 'upstream/master' into fix-2128-clean - 9e24b6e Merge branch 'master' into fix-2128-clean - f98cb4d Merge branch 'master' into fix-2128-clean * feat(browser): support allowed_private_hosts for browser tools (#8171) - cdf3fa7 feat(browser): support allowed_private_hosts for browser tools - 80d7f91 docs(browser): align allowed_private_hosts scheme doc with code - 6a5ecdf fix(browser): reject URL userinfo to close SSRF parser-mismatch - 66c0830 fix(browser): use reqwest::Url to close authority-boundary SSRF bypass - 3c84937 fix(browser_open): align scheme test and doc with relaxed http/https ...policy - d7c0921 Merge branch 'master' into worktree-browser-allow-private-hosts - b139b93 Merge branch 'master' into worktree-browser-allow-private-hosts * test(config): cover provider alias predicates (#8241) The China-provider alias predicates and `canonical_china_provider_name` in `provider_aliases.rs` had no unit coverage. Add a focused suite that locks down current behavior: - each family's alias set (GLM / Z.ai / MiniMax / Moonshot / Qwen and the standalone Qianfan / Doubao / Bailian families), including the global-vs-CN and intl-vs-CN splits; - that `is_qwen_alias` is the union of its CN/intl/US/OAuth sub-aliases and does not fold in the separate Bailian family; - `canonical_china_provider_name` mapping for every family plus its precedence, and a cross-check that the returned canonical name always agrees with the matching `is_*_alias` predicate; - that `family_honors_wire_api` returns true only for the bring-your-own-endpoint families (openai / llamacpp / custom). Test-only; no behavior change. * test(hardware): cover serial path allowlist helpers (#8245) The serial-path allowlist in `util.rs` gates which device paths the hardware serial peripheral may open, but it had no unit tests. Add coverage for: - `is_serial_path_allowed`: accepts each known prefix (/dev/ttyACM, /dev/ttyUSB, the usbmodem/usbserial families, COM) and rejects paths outside it, including a name missing the /dev/ prefix and the empty string; - `serial_path_allowlist_hint`: lists every prefix as a glob; - `should_open_serial_nonexclusive` / `serial_open_baud`: real device paths open exclusively at the configured baud. Test-only; no behavior change. * test(api): cover MediaAttachment classification (#8248) `MediaAttachment::kind` and `from_file` had no unit tests. Add coverage for the classification rules: - `kind` prefers a known media MIME type over the file extension, matches the MIME prefix case-insensitively, and falls back to the extension when the MIME type is uninformative (e.g. application/octet-stream); - extension-based classification across audio/image/video and the Unknown fallback, including case-insensitive and no-extension inputs; - `from_file` reads the bytes, derives file_name, maps the extension to a MIME type, and propagates a read error for a missing path. Test-only; no behavior change. * test(api): cover JSON-RPC request/response/error helpers (#8250) The JSON-RPC envelope types had no unit tests. Add coverage for: - `JsonRpcRequest::new` (sets the protocol version, wraps the id in `Some`) and the `#[serde(default)]` params field (omitted params deserialize to `Null`); - `JsonRpcNotification::new` (version set, no id field serialized); - `JsonRpcResponse` skipping `result` / `error` when `None`; - the standard JSON-RPC 2.0 error codes; - `JsonRpcError` serde round-trip including the optional `data` field. Test-only; no behavior change. * test(hardware): cover board registry vid/pid lookup edge cases (#8253) `lookup_board` had a happy-path test and an all-zero miss. Add the edge cases that guard the registry contract: - a match requires both VID and PID — a known VID with an unknown PID, or a known PID under the wrong VID, must miss (guards against the lookup degrading into a VID-only match); - the two PIDs registered under arduino-uno both resolve, and arduino-mega resolves under the shared Arduino VID via its own PID; - every (vid, pid) pair in the table is unique; - every known board resolves to itself via lookup_board. Test-only; no behavior change. * test(tools): cover web-search routing defaults and normalization (#8256) resolve_web_search_provider had tests for each provider alias and the unknown-provider fallback, but two contracts were uncovered: - "" and "default" route to DuckDuckGo with used_fallback = false — an explicit/empty default must be distinguished from an unknown-provider fallback, since the flag drives the warn-on-unknown path; - resolution trims surrounding whitespace and is case-insensitive. Test-only; no behavior change. * test(tools): cover node capability approval gating and group assembly (#8257) node_capabilities had coverage for names/descriptions/schemas and the sensitive-capability approval path, but two contracts were uncovered: - requires_approval matches the "camera."/"screen."/"location." prefix (with the trailing dot) and treats unknown / non-sensitive names as not requiring approval — a bare "camerafoo" must not match; - all_standard_capabilities assembles every group (camera, screen, location, and the system.notify notification capability). Test-only; no behavior change. * test(tools): cover truncate_with_ellipsis (#8259) util_helpers had tests for floor_char_boundary and clean_verbatim_path but none for truncate_with_ellipsis. Add coverage: - short or exactly-at-budget strings are returned unchanged (no ellipsis); - longer strings are cut to the char budget with "..." appended; - a trailing space in the kept slice is trimmed before the ellipsis; - the budget counts Unicode chars, not bytes ("héllo" at 2 -> "hé..."). Test-only; no behavior change. * test(memory): cover importance scoring weights and category bases (#8260) importance.rs tested the Core/Conversation base scores, the keyword boost, and weighted_final_score with symmetric inputs only. Add the uncovered contracts: - Daily (0.3) and Custom (0.4) category base scores; - weighted_final_score weights each signal independently (hybrid 0.7, importance 0.2, recency 0.1) — symmetric inputs cannot catch a weight swap; - keyword_boost is case-insensitive and yields no boost without signals. Test-only; no behavior change. * feat(sop): daemon SOP maintenance tick (EPIC A1) (#8391) Adds the periodic daemon tick that drives the SOP engine's maintenance, activating EPIC C's fail-closed approval timeout and B's retention/reaper. Engine: run_maintenance_tick() runs one pass - fire fail-closed approval timeouts, reap expired concurrency-claim leases, prune terminal runs past max_finished_runs - and returns a MaintenanceSummary for observability. The overdue-gate predicate is extracted into overdue_waiting_run_ids() and shared with check_approval_timeouts so the tick can count escalations. The tick self-approves nothing; timeout handling follows approval_timeout_action (default escalate). Config: sop.maintenance_interval_secs defaults to 60; 0 disables the tick. The resulting behaviors stay independently gated and fail-closed-safe. Daemon: spawn_sop_maintenance wires the tick into both daemon paths, behind agent-runtime, before the engine is moved into the registry. The returned JoinHandle is owned by the current foreground daemon/channel run and aborted when that run exits, so reloads do not leave stale maintenance loops on old SOP engine/config/store handles. Not included: firing cron-scheduled SOP triggers on the same tick (#6686). Executing an AutoApprove-resumed step needs the live SOP executor (EPIC A2); until then the tick logs it. * feat(sop): wire cron triggers into maintenance tick (#8400) Wires cached SOP cron triggers into the daemon/channel maintenance tick, giving configured [[triggers]] type = "cron" SOPs a production caller. Threads the shared SOP audit logger into the maintenance task and records cron started/skipped/no-match counts alongside the maintenance summary. Rebuilds the maintenance/cron task per daemon reload and aborts the old task when the foreground runner exits. Deduplicates fired cron expressions per check window so SOPs sharing one expression dispatch once. Fixes #6686. Depends on #8391. * feat(sop): execute live SOP steps (#8399) - 6ad3212 feat(sop): execute live SOP steps - f8d5141 fix(sop): avoid duplicate live executor metrics * feat(plugins): wasmtime component-model host for tool/channel/memory (#8368) - 8be579c feat(plugins): wasmtime component-model tool host, retire extism - cf45797 feat(plugins): channel and memory component-model adapters - e0e8b2b fix(plugins): wire sandboxed WASI p2 into plugin linkers - 071ca34 fix(plugins): route pulley backend through the component compile path - d501f5d fix(plugins): route pulley through deserialize arm, not compile arm - 0d0b683 fix(plugins): link logging host import for all three plugin worlds - bf2a757 test(plugins): end-to-end reference plugin load from a throwaway config dir - c9f2900 test(plugins): refresh reference fixture from refactored published source - 04f18fe fix(plugins): surface channel poll-message traps instead of silent idle - b2634d6 docs(plugins): document the wasmtime component-model plugin host - fa41882 test(plugins): drop committed reference-plugin.wasm binary fixture - 3ce4ae7 fix(docs): point plugin-protocol config link at tracked reference index Co-authored-by: bheatwole <5197535+bheatwole@users.noreply.github.com> Co-authored-by: JordanTheJet <24375129+JordanTheJet@users.noreply.github.com> * feat(sop): add step contract substrate (#8416) - 5eb6d0e feat(sop): add step contract substrate - 230192b docs(sop): document step contract substrate - b8ea2e2 fix(sop): preserve policy denies for mandatory tools - 306e82e fix(sop): add ..SopStep::default() to cron regression test fixture * feat(sop): enforce step schemas at engine boundary (#8420) - 0e9c5f0 feat(sop): enforce step schemas at engine boundary - 502427b docs(sop): document step contracts - 2d48b37 test(sop): satisfy schema enforcement lint * feat(sop): enforce step routing (#8430) - ca8ebd6 feat(sop): enforce step routing - 4aff05b fix(sop): persist routed checkpoint state - 05c8f6d fix(sop): count retries after initial failure * test(runtime): cover turn-loop cancellation error types and chain detection (#8456) - 4e987a9 test(runtime): cover turn-loop cancellation error types and chain det… - 4e8040c test(runtime): replace disallowed anyhow! macro with anyhow::Error::msg - 5e6212f Merge branch 'master' into alix/test-turn-outcome * test(rpc): cover locale parsing and frame schema validation (#8489) - e3fd312 test(rpc): cover locale parsing and frame schema validation - 7d5d9db Merge branch 'master' into test/rpc-types-locales-coverage * fix(tools/calculator): cap values array length to prevent OOM (#8481) - ab0684f fix(tools/calculator): cap values array length to prevent OOM - 7df1498 Merge branch 'master' into fix/calculator-values-cap * feat(cost): cost/org snapshot RPC + cost/query [from,to) window (#8482) - 80c78c6 feat(cost): cost/org snapshot RPC + cost/query [from,to) window - 07c3773 fix(cost): cost/org errors on unreadable snapshot, null only for NotF… - 834aca6 fix(cost): clippy struct-update in test helper; document cost/query a… - 6bffe0d test(cost): bounded cost/query RPC coverage (valid bounds + INVALID_P… - 1ba8778 Merge branch 'master' into feat/cost-org-rpc * feat(channels): add passive WhatsApp group context (#8389) - 963ac53 feat(channels): add passive WhatsApp group context - 53a76b0 Merge branch 'master' into feat/8379-passive-group-context - 86a9b78 test(channels): add new ChannelMessage fields to merged telegram test… - 15ef06f Merge branch 'master' into feat/8379-passive-group-context - b9ad1b6 fix(plugins): add new ChannelMessage fields to wasm channel inbound c… - 8719982 Merge branch 'master' into feat/8379-passive-group-context * fix(deps): bump anyhow for RUSTSEC-2026-0190 (#8500) * fix(tool-call-parser): replace Regex::new().unwrap() with expect() (#8388) Replace 14 instances of .unwrap() on Regex::new() in LazyLock initializers with .expect() carrying the static variable name, so a broken regex pattern during refactoring produces an actionable panic message. * ci(workflows): add Trivy container scanning for PR and release images (#8168) - f75d74d ci(workflows): add Trivy container scanning for PR and release images - de1afa7 fix(ci): split trivy sarif upload - c806742 Merge branch 'master' into ci/trivy-container-scanning - 913efc4 Merge branch 'master' into ci/trivy-container-scanning * feat(runtime): add calendar no-show SOP triggers (#8419) - af5b68b feat(runtime): add calendar no-show SOP triggers - 866da50 Merge remote-tracking branch 'origin/master' into codex/issue-6074-ca… - 2e9c1a3 Merge branch 'master' into codex/issue-6074-calendar-no-show Co-authored-by: Nim G <theredspoon@users.noreply.github.com> * feat(cost): offline pricing catalog (consume-only $0 fallback) (#8380) - 83290ec feat(cost): offline pricing catalog, consume-only, $0 fallback - 2da8713 build: revert Cargo.lock to master (catalog adds no new dependency) - d45b8e8 fix(cost): clear global pricing catalog when pricing.json is removed * fix(channels): preserve image bytes for a configured vision_model_provider (#8468) The media pipeline only embedded base64 image data when the default model provider reported vision support. When [multimodal].vision_model_provider routes images to a dedicated vision provider, the pipeline stripped the bytes to a filename ref before the vision route ran, so the vision provider never saw the image. Gate the media-pipeline vision flag on the configured vision route too. Adds a wiremock regression: a non-vision default provider plus a configured vision route must deliver the base64 bytes to the vision provider, fails on the old gate, passes with the fix. Closes #6841 * feat(sop): enforce step scope and mode events (#8493) - 6ad3212 feat(sop): execute live SOP steps - 5eb6d0e feat(sop): add step contract substrate - 230192b docs(sop): document step contract substrate - 0e9c5f0 feat(sop): enforce step schemas at engine boundary - 502427b docs(sop): document step contracts - 2d48b37 test(sop): satisfy schema enforcement lint - ca8ebd6 feat(sop): enforce step routing - 4aff05b fix(sop): persist routed checkpoint state - b8ea2e2 fix(sop): preserve policy denies for mandatory tools - 34fc751 fix(sop): avoid duplicate live executor metrics - 05c8f6d fix(sop): count retries after initial failure - ba1d50a feat(sop): enforce step tool scope - d2187a6 feat(sop): emit step mode transition events * feat(sop): complete payload safety ingress (#8502) * fix(ci): remove stray zero-to-5 gitlink (#8494) * fix(tools): avoid manual Windows verbatim prefix slicing (#8497) * fix(channels): add missing ChannelMessage fields in orchestrator test (#8511) * docs(labels): document held scoped labels (#8484) - efb5f17 docs(labels): document held scoped labels - 3f87fca docs(labels): clarify scoped label pairing * feat(sop): add filesystem SOP event source (#8461) - 3f1c2d9 docs(sop): list all eight denied broad roots in connectivity - d98e27e fix(channel-filesystem): make content cap bite by default, bound debounce map - f531e12 fix(channels): add new ChannelMessage fields to vision-route test - cc00cd8 feat(channel-filesystem): carry a 64 KiB content cap, gated off by default - 1841792 fix(channel-filesystem): extend broad-root deny list to pseudo-filesystems - 94b4633 fix(channel-filesystem): fail closed on invalid globs and stream file hashing - a86c233 feat(channel-filesystem): make max_content_bytes optional, default unlimited - 31ee870 fix(channel-filesystem): reject symlink event paths by default - 0745dec fix(sop): drive headless deterministic runs to terminal so channels refire - d7da22b refactor(sop): make filesystem source a Channel impl - febf82c feat(sop): add filesystem SOP event source * ci(workflows): add cargo-audit step to PR gate, sync advisory ignores (#8129) - baca7f9 ci: add cargo-audit to PR gate, refactor ci.yml with YAML anchors - 9373b87 fix(ci): remove invalid workflow anchors * test(plugins): cover WasmTool accessors and default schema (#8273) - be7c73c test(plugins): cover WasmTool accessors and default schema - be24e61 Merge branch 'master' into alix/test-plugins-wasm-tool * feat(nix): repair zeroclaw/zerocode nix builds, add nix hash updates to release process (#8336) - c0f0409 fix(flake): repair zeroclaw, zerocode nix builds - 34b06ba Merge branch 'master' into flake-updater - 6127054 Extract CI hash check to script - 4c14416 Address review comments * fix(channels): replace Regex::new().unwrap() with expect() in orchestrator and consolidation (#8361) - orchestrator/mod.rs: TOOL_RESULT_RE regex unwrap -> expect - consolidation.rs: media-tag regex unwrap -> expect * fix(zerocode): clear transcript highlight on input click (#8472) * fix(providers/openai-codex): gate has_tools on non-empty list (#8476) - 1f4e47b fix(providers/openai-codex): gate has_tools on non-empty list (fixes … - 8618a67 test(providers/openai-codex): drive regression through the production… * fix(deps): bump strum versions to deduplicate lockfile (#8225) - 6a5d82d fix(deps): bump lru and strum versions to deduplicate lockfile - d557b1f fix(deps): bump strum 0.27→0.28 to deduplicate lockfile - 72b5c3a fix(deps): drop lru bump * test(config): cover postgres backend field-visibility exclusions (#8450) * test(config): cover ThinkingLevel parsing, as_str roundtrip, and budget token thresholds (#8454) * perf(channels): bound orchestrator notify channel + cap path/url body (#8460) The `ChannelNotifyObserver` had two latent OOM vectors, both flagged by the audit at audit-zeroclaw-2026-06-28.md: 1. **Unbounded mpsc** (`crates/zeroclaw-channels/src/orchestrator/mod.rs:4570`) used `tokio::sync::mpsc::unbounded_channel::<String>()` for tool-event notifications. A slow downstream channel (e.g. a stalled Discord / Slack API call at the consumer task at lines 4585-4597) could buffer strings indefinitely. A 1000-tool-call turn against a slow channel blows up RSS. Switched to `channel::<String>(128)` (matches the mid-range of the file's existing production mpsc capacities) and the producer now `try_send`s and drops on `Full` / `Disconnected`. 2. **Unbounded per-message body** — the `path` and `url` branches in the observer's `record_event` impl (`orchestrator/mod.rs:171-174`) used `format!(": {p}")` / `format!(": {u}")` with no cap, while the surrounding branches (command / query / generic) already routed through `truncate_with_ellipsis`. A user-controlled 10 MB path or URL would pass through verbatim. Both branches now route through `truncate_with_ellipsis(p, NOTIFY_DETAIL_MAX_CHARS)` with a 4096-char cap (4 KiB — large enough to cover any realistic absolute workspace-prefixed path, small enough to bound the worst case). `NOTIFY_DETAIL_MAX_CHARS` is a private const at the top of the impl block so the cap is visible at the call site and easy to bump. `truncate_with_ellipsis` and `floor_char_boundary` are reused unchanged from `crates/zeroclaw-channels/src/util.rs:2,13` — no new helpers, no new deps. Durability-of-UX tradeoff (documented in the comment on the producer push): live-typing notifications are best-effort UX. A dropped message degrades the indicator briefly but does not lose any real state. We do not log on drop because the observer is in a hot path and a slow channel could generate thousands of drops per second; the existing observer-hook call sites in the file already follow the no-log-on-drop convention. Two new regression tests pin the contracts: - `channel_notify_observer_caps_long_path_argument` — feeds a 64 KiB `path` argument (16x the cap) and asserts the emitted string is bounded by `NOTIFY_DETAIL_MAX_CHARS + prefix + ellipsis`. - `channel_notify_observer_drops_on_full_channel` — pushes two events into a capacity-1 channel without draining; asserts exactly one arrives and `tools_used` reflects both events were observed on the observer side (the drop is on the notify side, not the observer side). The existing `channel_notify_observer_truncates_utf8_arguments_safely` test was updated to construct a bounded channel to match the new field type; no test-logic change. Public API impact: none. `ChannelNotifyObserver` is module-private (no `pub`, no re-export); only its internal field type changed. `zeroclaw-channels` is `Experimental` per AGENTS.md so there is no stability concern regardless. Validation: cargo fmt --all -- --check clean cargo clippy --locked -p zeroclaw-channels --all-targets -- -D warnings 0 cargo test --locked -p zeroclaw-channels --lib -- orchestrator:: 344 passed (2 new) cargo build --locked clean * ci(npm): add daily audit sweep (#8480) * ci(workflows): add SLSA provenance attestation to release pipeline (#8277) - 5f24e4d ci(workflows): add SLSA provenance attestation to release pipeline - 219cdbb ci(release): add SLSA provenance attestation - 4cf978c fix(ci): harden offline attestations * feat(config): configurable native runtime shell (ref: #5246) (#8311) - 9138377 feat(config): configurable native runtime shell (#5246) - 144efc9 fix(config): validate native runtime.shell and prove execution under it - 4a0ce2a fix(config): reject relative-path runtime.shell and document the rules - 34be98b test(config): platform-gate native_with_shell_passes_c_flag - 4bdc9eb Merge remote-tracking branch 'origin/master' into configurable-shell - ab64ae3 test(config): use target_os cfg form for shell boundary test - 57dd2fa Merge branch 'master' into configurable-shell * fix(fill-translations): drop stale translations key in leak repair (#8312) (#8322) The trailing-\n pre-pass at main.rs:393 seeds translations[msgstr_line] with the entry's current msgstr text when msgid ends with \n. The subsequent leak-repair pass rewrites only lines[entry.msgstr_line] (Recovered translation or blanked Unrecoverable) but never updates or removes the matching translations entry. write_po prefers translations over lines, so the pre-seeded leaked text wins and re-ships the leak to disk even after the lines-side repair cleared it. For a Recovered leak this is permanent (the entry is never re-translated); for an Unrecoverable leak whose later re-translation fails, the stale value also wins. Drop translations[msgstr_line] in both Recovered and Unrecoverable arms of the leak-repair loop so write_po falls through to the correctly-repaired lines value. The pre-pass and write_po are unchanged. Orthogonal to #8039 / #8118 (orphaned continuation lines) — that fix targets lines-side re-parse contamination; this fix targets the translations-map stale-key path. Both modify the same loop site but address independent failures. Regression test repair_leaks_drops_stale_translations_key drives write_po end-to-end with the trailing-\n pre-seeded key in place and asserts the recovered translation wins. Mutation-checked: removing the translations.remove(...) line makes the test fail. Closes #8312. # Conflicts: # tools/fill-translations/src/main.rs * refactor(ci): consolidate prebuilt Docker image variants (#8485) Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com> * fix(ci): reconcile cargo-audit ignores with deny.toml to unblock master (#8520) The `cargo audit` CI step reads the entire Cargo.lock and was failing master on 22 RustSec advisories absent from `.cargo/audit.toml`, while `cargo deny check` (graph-aware, reads deny.toml) tolerated most of them. The two advisory tools had drifted out of sync. Add all 22 to both files with reasons referencing tracking issue #8519 (milestone v0.8.4): - 3 wasmtime-wasi CVEs (RUSTSEC-2026-0149/-0182/-0188) pulled via zeroclaw-plugins. The real fix is a wasmtime 43 to 45.x stack bump, deferred to #8519; ignored here only to unblock the gate. Dependabot PR #8490 (bump to 44.0.2) is insufficient: -0182 needs >= 44.0.3 and -0188 needs >= 45.0.3. - 19 unmaintained-crate advisories with no available fix: GTK3 stack via zeroclaw-desktop, unic-* unicode tables, proc-macro-error/2, derivative, and ttf-parser (via pdf-extract to lopdf), all transitive. Validated: `cargo audit` exit 0, `cargo deny check` exit 0. Related #8519 * fix(gateway): propagate pairing DB errors instead of panic (#8466) - 240fc6d fix(gateway): make pairing atomic across DB errors - 3b806ac Merge branch 'master' into fix/api-pairing-propagate-db-errors * test(runtime): cover TrustTracker score updates, decay, regression, and autonomy reduction (#8457) - cfdc589 test(runtime): cover TrustTracker score updates, decay, regression, a… - b421e94 test(runtime): make regression preconditions unconditional assertions - 0e6c7fb Merge branch 'master' into alix/test-trust-types * test(eval): cover EchoTool metadata, schema, execute output, and default tool registry (#8459) - d593b6e test(eval): cover EchoTool metadata, schema, execute output, and defa… - 7fb08c6 Merge branch 'master' into alix/test-eval-tools * fix(channels): enable model commands on WhatsApp Web (#8414) - b1d2564 fix(channels): enable model commands on WhatsApp Web - 3faa907 Merge branch 'master' into codex/fix-whatsapp-runtime-model-command * test(eval): cover RecordingObserver tool recording and token accumulation (#8458) - 24e03a7 test(eval): cover RecordingObserver tool recording and token accumula… - c07b450 Merge branch 'master' into alix/test-eval-observer - 101dac7 fix(eval): update observer test event shape * fix(anthropic): propagate serialization error in streaming request builder (#8148) - fc681f2 fix(anthropic): propagate serialization error in streaming request builder - c45d387 fix(anthropic): add missing anyhow::Context import and fix StreamError type - c2c909d fix(providers): apply cargo fmt for CI format gate - 38cc88f fix(channels): add missing Default::default() to ChannelMessage test initializer - 4b0e9d1 chore: cargo fmt - 6e6acd7 Merge branch 'master' into fix/anthropic-streaming-request-expect * fix(runtime): fire session_end hook on session termination (fixes #7889) (#8003) - 16a34ac fix(runtime): fire session_end hook on session termination (fixes #7889) - f064da8 fix(runtime): register configured hook handlers in RPC path, gate session_end on existence - 26243d6 fix(runtime): extract shared HookRunner::from_config, collapse clippy, add tests - 0cb0385 fix(runtime): correct command-logger handler name assertion in test - 1aa5a35 fix(runtime): rebase session_end hook onto latest master, add missing hooks field - a37207e fix(runtime): apply cargo fmt for CI - a5f3996 fix(runtime): run cargo fmt --all for CI format gate - 859d733 test(dispatch): prove missing-session close/delete does not fire session_end - decb565 test(dispatch): add positive regression for session_end hook on real session close - 4ba12cb style: cargo fmt - 663d523 fix(dispatch): add missing hooks field to minimal_with_cost_tracker - 010eec1 fix(channels): add missing Default::default() to ChannelMessage test initializer - 143136d Merge branch 'master' into fix/session-end-hook-fire * feat(plugins): per-call execution limits and FND-001 backend taxonomy (#8491) - d4a4165 feat(plugins): per-call execution limits and FND-001 backend taxonomy - 45d8bd3 fix(plugins): validate all four limit fields and refresh plugin docs - 57f99dc fix(plugins): pass PluginLimits in WasmTool::new unit test * chore(desktop): remove the zeroclaw-desktop Tauri app and all wiring (#8544) - 2417999 chore(desktop): remove the zeroclaw-desktop Tauri app and all wiring - 884e3d8 chore(desktop): clear leftover desktop references from retirement - 0b1642c chore(desktop): strip remaining desktop release references * fix(config): warn when sqlite memory requests vector search without an embedder (#8501) memory.backend defaults to sqlite and memory.search_mode defaults to Hybrid, but embedding_provider defaults to none. The runtime resolves none to NoopEmbedding (dimensions 0), so the vector path is silently skipped and recall collapses to keyword/LIKE-fallback with no warning, log, or doctor signal. Add a non-fatal warning in Config::collect_warnings() that fires when: - the memory backend family is sqlite (derived from memory.backend, not resolve_active_storage(), so a bare backend = "sqlite" is covered), - search_mode is hybrid or embedding, and - no effective embedder is configured. A valid hint: embedding route counts as an effective embedder even when embedding_provider stays none. The check derives entirely from existing [memory] and [[embedding_routes]] state; nothing new is persisted. The warning surfaces on stderr via validate(), through the gateway PropResponse/PatchResponse, and at zeroclaw doctor. Warning-only, no behavior change. The runtime dimensions() == 0 follow-up is deferred per maintainer guidance while #8382 reworks zeroclaw-memory. * feat(web): add select all/deselect all toggle to tool picker groups (#8464) Add per-group "Select all" / "Deselect all" controls to the dashboard ToolPicker. Each control acts on its group's currently-displayed (filtered) entries so it honors an active search and matches the count shown in the group header, and its label flips between "Select all" and "Deselect all" based on whether every displayed entry is already selected. Selection stays order-preserving and de-duplicated. The controls render in a toolbar above the list, outside the role="listbox", so they are valid ARIA (a listbox may only contain option/group descendants) and reachable in the natural Tab order with native button activation. New en i18n strings (select_all, deselect_all, and their aria-label prefixes) drive the labels. * feat(amqp): SOP fan-in dispatch path, fan-in usage docs, and AMQP credential secret fix (#8521) - bb0286b feat(amqp): add SOP fan-in dispatch path - 011f4cf docs(sop): schema-driven SOP fan-in pages and AMQP channel docs - e6ce719 fix(config): mark AMQP credential fields secret - 88d218f docs(sop): make fan-in pages answer how to actually use each source - fe9dc8a fix(docs): repoint sop-unwired snippet links and exclude generated re… - 17f2789 fix(amqp): fail closed on SOP dispatch routing * docs(labels): document line and memory backend labels (#8523) * feat(config): add local_small runtime preset (#8531) - 9dabcab feat(config): add local_small runtime preset - ad6831d fix(config): enable local_small tool loop - af268e5 Merge branch 'master' into codex/issue-5287-local-small-preset * test(tools): cover weather HTTP skill shape (#8433) - 3726f86 test(tools): cover weather HTTP skill shape - 9fb0d2a Merge branch 'master' into codex/issue-6165-weather-skill-proof - 0678bd7 Merge branch 'master' into codex/issue-6165-weather-skill-proof * fix(ci): allow generated docs reference links (#8533) - bec9dca fix(ci): allow generated docs reference links - ed9b0a8 Merge branch 'master' into codex/fix-docs-link-gate-generated-refs * ci(workflows): gate Windows Clippy for tools changes (#8517) - a6fc04f ci(workflows): gate Windows Clippy for tools changes - e83dcb3 ci(workflows): scope targeted Windows Clippy to tools - f88d922 Merge remote-tracking branch 'origin/master' into codex/ci-targeted-w… - 2b4c7c2 ci(workflows): keep targeted Windows Clippy dependency-local - 98f8145 Merge branch 'master' into codex/ci-targeted-windows-clippy * fix(docs): autolink ACP elicitation RFD (#8498) - 089995f fix(docs): autolink ACP elicitation RFD - 61b270b Merge branch 'master' into codex/issue-7269-rustdoc-bare-urls - 870f447 Merge branch 'master' into codex/issue-7269-rustdoc-bare-urls * docs(security): clarify tool receipt guarantees (#8407) - 3f94f54 docs(security): clarify tool receipt guarantees - 0397b0b Merge branch 'master' into codex/security-receipts-doc-contract * fix(i18n): reject local path leaks in translations (#8365) - 8b25b21 fix(i18n): reject local path leaks in translations - 47ecfa6 fix(i18n): satisfy mdbook path leak lint - 8f2fb23 Merge remote-tracking branch 'origin/master' into codex/issue-6407-ab… - 21c3662 Merge branch 'master' into codex/issue-6407-absolute-path-guard-r2 * feat(skills): suggest missing plugins from cached registry (#8428) - feat(skills): suggest missing plugins from cached registry - fix(skills): satisfy ci lint for plugin suggestions * fix(channels): fire message_sent hooks after delivery (#8355) - fix(channels): fire message_sent hooks after delivery - fix(channels): refresh message_sent hook test fixtures * ci(workflows): guard declared repository submodules (#8516) - ci(workflows): guard declared repository submodules * fix(agent): refresh system prompt TASK_FRAMING anchor per-turn for vision-routed providers (#8054 Surface 3) (#8503) - fix(agent): refresh system prompt TASK_FRAMING anchor per-turn for vision-routed providers (#8054 Surface 3) * fix(gateway): advertise A2A cards on runtime port (#8538) - fix(gateway): advertise A2A cards on runtime port * fix(runtime): gate Unix-only shell test helper behind #[cfg(unix)] (#8535) - fix(runtime): gate Unix-only shell test helper behind #…
Summary
master(commit74a806943)delete_agent_cascaderuns the archive + owned-state cascade beforepersist_and_swap. A persist failure leaves config naming the agent while its workspace has been archived and its owned stores (memory / cron / acp / session) have been purged — the inverse of the [Bug]: agent rename can move owned state before config persistence #7907 rename split-brain, in the delete direction.warningsfield on the response.crates/zeroclaw-gateway/src/api_config.rs::delete_agent_cascade— reorder:delete_with_cascade+ mark dirty +persist_and_swapfirst, then archive +cascade_owned_statefrom the post-swap config.crates/zeroclaw-gateway/src/api_config.rs::MapKeyResponse— addwarnings: Vec<String>(#[serde(default, skip_serializing_if = "Vec::is_empty")]).crates/zeroclaw-gateway/src/api_config.rs::tests— three real-sqlite regression tests:agent_delete_leaves_owned_state_intact_when_persist_fails(cron probe)agent_delete_purges_owned_state_after_successful_persist(cron probe + workspace-archive check)agent_delete_response_carries_partial_failure_warnings(read-only archive root →warningsfield in 200 OK)crates/zeroclaw-gateway/src/api.rs—test_statebecomespub(crate)and is re-exported at the crate root viapub(crate) use tests::test_state;, mirroring the helper Nillth added for the [Bug]: agent rename can move owned state before config persistence #7907 rename coverage. Without this, the new regression tests inapi_configcan't reach the AppState builder.Label Snapshot
Change Metadata
Linked Issue
Closes #7941
Related: #7907 (the rename-direction mirror of this defect)
Supersede Attribution
None.
Validation Evidence
$ cargo fmt --all -- --check (no output — clean) $ cargo clippy --locked -p zeroclaw-gateway --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 21.89s $ cargo test --locked -p zeroclaw-gateway --lib test result: ok. 266 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out $ cargo test --locked -p zeroclaw-gateway --lib api_config::tests::agent_delete test api_config::tests::agent_delete_leaves_owned_state_intact_when_persist_fails ... ok test api_config::tests::agent_delete_purges_owned_state_after_successful_persist ... ok test api_config::tests::agent_delete_response_carries_partial_failure_warnings ... ok test result: ok. 3 passed; 0 failedReal behavior proof
DELETE /api/config/map-key?path=agents&key=<alias>must leave the agent's workspace and owned stores intact on a persist failure, and must surface partial side-effect failures in the response body (not just the server log).74a806943.cargo test --locked -p zeroclaw-gateway --lib api_config::tests::agent_deletecargo test --locked -p zeroclaw-gateway --lib(full crate lib tests, 266 passed)cargo clippy --locked -p zeroclaw-gateway --all-targets -- -D warnings(clean)cargo fmt --all -- --check(clean)rusqlitecron probes (not mocks). The persist-fail test makesconfig_pathitself a directory sosave_dirty's atomic write cannot complete; the cron row stays undervictimand the workspace directory is never created. The happy-path test asserts the cron row is purged, the workspace is gone from its original path, and avictim-<ts>archive directory exists with aworkspace/subdir. The partial-failure test chmods the archive root to0o555(unix-only) and asserts the response body has awarningsarray with at least onearchivemention.warningsfield (omitted when empty). The persist-fail / persist-ok / partial-fail invariants from the issue body all hold.delete_agent_cascadefunction directly with a constructedAppState. The HTTP layer is a thin axum wrapper; the cascade logic that the issue calls out is fully covered.Security Impact
require_authupstream (unchanged).fs::rename, and the per-store purges are the same as before — only the ordering changed. The persist-first reorder means a partial-failure window now showsconfigalready deleted while workspace / owned stores may still be present, so the operator MUST inspect thewarningsfield. This is documented in the response struct's doc comment.Privacy and Data Hygiene
data_dir/agents/_deleted/<alias>-<ts>/and continues to receive the owned-state export.Compatibility / Migration
MapKeyResponsegains an optionalwarnings: Vec<String>field. The field is#[serde(default, skip_serializing_if = "Vec::is_empty")], so the wire format is byte-identical for the non-agentMapKeycallers (generic create / post-replace). Only the agent-delete path can populate it.path/key/created: falsebody. A persist failure still returns the same error response (4xx withConfigApiCode::ReloadFailed). The new behavior is strictly additive on the response shape.warningsfield; new clients can act on it.i18n Follow-Through
No user-visible string changes. The
warningsarray content is operator-facing English (e.g."archive dir creation failed (...): <io error>"); it does not pass through Fluent.Human Verification
cargo test --locked -p zeroclaw-gateway --liblocally — 266 passed, 0 failed.persist_and_swapbetween thedelete_with_cascadeblock and the archive block; theworkingmove intopersist_and_swapis the last use ofworkingbefore the post-swap read-back ofstate.configfordata_dirand the cascade.Side Effects / Blast Radius
MapKeyResponseschema gains an optional field. The OpenAPI generator (schemars::JsonSchema) re-derives the schema from the struct, so the generated OpenAPI document now listswarningsas an optional array of strings..githooks/runsrust_quality_gate.sh+cargo test --locked— both pass on the affected crate.Agent Collaboration Notes
test_statevisibility change (fn→pub(crate)+pub(crate) use tests::test_state;) is the same pattern Nillth added toapi.rsin PR fix(gateway): persist agent rename before moving owned state #7940. If that PR lands first, this PR's visibility change is a no-op (the symbol is alreadypub(crate)); if it lands second, Nillth's diff will overlap and one of us will need to rebase. The diff is two lines either way; rebase is trivial.warningsfield onMapKeyResponseis parallel to the existingwarningsfield onRenameMapKeyResponse(line 1282). If the maintainer prefers a single shared type, that's a follow-up refactor.Rollback Plan
MapKeyResponse::warningsis#[serde(default, skip_serializing_if = "Vec::is_empty")]so rolling back leaves the wire format unchanged for non-agent callers.Risks and Mitigations
MapKeyResponseoutsideapi_configbreaks because the new field is mandatory in construction. Mitigation: grep confirmed all three call sites —delete_agent_cascade(this PR),handle_map_key(created + replaced, both updated), and the create-on-empty path. All four are updated. If a downstream consumer constructsMapKeyResponsevia struct-literal, they need to addwarnings: vec![]. The compiler will catch any miss.test_statevisibility change inapi.rswidens the test-only API surface. Mitigation:#[cfg(test)]on both thepub(crate) useline and thepub(crate) fn— production builds are unaffected.#[cfg(unix)]chmod); the windows behavior is unverified. Mitigation: the test is#[cfg(unix)]-gated so the CI macOS + Linux runners cover it; the windows runner is not in the project's required-CI matrix. The cascade code itself is platform-agnostic — only the failure-injection mechanism is unix-specific.